#typescript #generics
#typescript #общие сведения
Вопрос:
Я пытаюсь найти типобезопасный способ написания следующей функции:
const merge = <A, B, C>(a: A, b: B): C => ({...a, ...b});
Очевидно, что это не компилируется, и я этого не ожидаю, это просто для того, чтобы показать, что функция должна возвращать объединение двух входных данных, где b
имеет приоритет над a
в случае перекрытия.
Я нашел статью, в которой описывается типобезопасный способ объединения двух объектов. Короче говоря, это модифицированная версия статьи:
type Omit<T, U> = Pick<T, Exclude<keyof T, keyof U>>;
type Defined<T> = T extends undefined ? never : T;
type MergedProperties<T, U> = {
[K in (keyof T amp; keyof U)]:
undefined extends T[K]
? Defined</* T[K] | */ U[K]>
: T[K]
};
const merge = <T extends object, U extends object>(t: T, u: U) => ({
...t,
...u
} as Omit<T, U> amp; Omit<U, T> amp; MergedProperties<U, T>);
Это означает, что код компилируется при правильном использовании, то есть два типа входных данных имеют тип A
и B
, а выходные данные имеют тип C
в следующем фрагменте кода:
type A = { a: string; b: number };
type B = { /* no a */ b: string, c: number };
type C = { a: string, b: string, c: number };
Итак, учитывая, a
b
и c
чьи соответствующие типы являются их типом с заглавной буквы, я могу написать это:
const c: C = merge(a, b);
Проблема
Если я изменю либо тип, B
либо type C
(например, добавив новое свойство), компилятор пожалуется, что C
(присвоение) не удовлетворяет заданному условию. Чего я хочу, так это чтобы компилятор пожаловался, что входные данные неверны.
Хорошо, итак, я заранее определяю свой тип вывода и ожидаю, что функция соответствующим образом настроится в этой форме:
const merge = <C>() => <A, B>(a: A, b: B): C => ({...a, ...b});
Теперь мне нужно настроить A
и B
, чтобы сказать это A
и B
выдать C
. Я полагаю, это сработало бы:
type Omit<T, U> = Pick<T, Exclude<keyof T, keyof U>>;
const merge = <C>() => <A extends Omit<C, B>, B extends Omit<C, A>>(
a: A, b: B
): C => ({ ...a, ...b});
Значение: если я исключу каждый ключ из A
, который определен в B
, и каждый ключ из B
, который определен в A
, но по отдельности они расширяются C
, вместе они выдают тип C
— с объединенными типами в случае перекрытий — (или я получаю бесконечную рекурсию (?)).
Это говорит TS2322: Type 'A amp; B' is not assignable to type 'C'.
лишь о многом.
Если я изменю A
на A extends C
, это сработает.
Если я изменяю A
на A extends Omit<C, never>
, это не так, и я не понимаю почему. (Это должен быть тип, C
из которого я ничего не исключал.)
Итак, как я могу определить типы A
и B
так, чтобы они оба вместе выдавали C
(общие данные при вводе функции без параметров), чтобы при изменении любого из входных данных в возвращаемой функции компилятор жаловался на входные параметры, а не на выходные параметры?
Комментарии:
1. У вас это сработает, если вы измените свой second
merge
на<C>() => <A extends Omit<C, B>, B extends Omit<C, A>>(a: A, b: B) => ({ ...a, ...b}) as unknown as C
? То есть использовать утверждение типа? Вы уже делаете это с оригиналом,merge()
который возвращает({ ...t, ...u}) as Omit<T, U> amp; Omit<U, T> amp; MergedProperties<U, T>)
. Или вам нужно что-то еще?2. Мне также немного неясны ваши варианты использования. Вы хотите
merge<C>()({ a: false }, { a: false, b: "", c: 0 })
выдать ошибку или быть принятым?3. Я немного сбит с толку, потому что это работает так, как вы только что написали, но я не совсем понимаю это. Я думал, что следует использовать утверждение типа 1) : если возвращаемый тип находится
any
в лямбда-функциях или 2) : компилятор не может угадать возвращаемый тип иas
принудительно приводит значение. Почему система типов не может угадать это самостоятельно, зачем нужен «взлом»? Развеas unknown as C
это не взлом? (Я мог бы также написатьas any as C
.) Более того, почему он ведет себя по-разному дляA extends C
в отличие отA extends Omit<C, never>
, хотя технически они одинаковы?4. Это было бы конечной целью, да, если бы это выдавало ошибку, но сначала я хотел создать базовую настройку. Кстати, что касается многих вопросов, просто очень сложно понять эти продвинутые типы.
5. Система типов не может вычислить это либо потому, что для этого требуется интеллект на уровне человека, либо потому, что компилятору не стоило бы постоянно пытаться делать такие предположения (большое снижение производительности только при случайной выгоде). Возможно, вам захочется подумать о том, как бы вы запрограммировали компилятор, чтобы выяснить это. Здесь правильный путь — утверждения; я думаю, что называть это «взломом» — это вопрос мнения. Конечно, они были введены в язык для случаев использования, точно подобных этому.
Ответ №1:
Вот как я бы поступил с вашими заявленными требованиями.
Сначала создайте тип, который представляет результат объединения двух типов, чтобы мы могли легко обращаться к нему позже:
type Merged<T extends object, U extends object> =
Omit<T, U> amp; Omit<U, T> amp; MergedProperties<U, T>;
Теперь давайте определим вашу функцию curried merge()
. Я собираюсь назвать это mergeConstrained()
:
const mergeConstrained: <C>() =>
<A extends object, B extends object>(
a: A,
b: B amp; (Merged<A, B> extends C ? B : C)
) => Merged<A, B>
= () => (a, b) => ({ ...a, ...b }) as any;
Обратите внимание на тип b
is B amp; (Merged<A, B> extends C ? B : C)
. Наличие B amp; ...
заставляет компилятор выводить B
, что это тип переданного b
параметра, точно так же, как если бы тип b
был простым B
. Как только он делает этот вывод, он оценивает пересечение. Второй составляющей пересечения является условный тип, который проверяет, Merged<A, B>
может ли он быть присвоен C
. Если это так, то b
это приемлемо, и пересечение становится B amp; B
или просто B
. В таких случаях ошибок выводиться не будет. Однако, если Merged<A, B>
это не присваивается C
, значит, вы передали неверный b
параметр… и пересечение становится B amp; C
. В таких случаях b
параметр не будет присваиваться C
, и вы получите сообщение о том, где что-то пошло не так.
И возвращаемый тип — это Merged<A, B>
вместо C
, так что вы не потеряете информацию о типе любых дополнительных переданных свойств. На самом деле, поскольку вы, вероятно, предпочли бы не смотреть на выходные типы, подобные Merged<{a: string, d: boolean}, {b: string, c: number}>
, а предпочли бы {a: string, b: string, c: number, d: boolan}
, существует более сложная версия mergeConstrained()
, которая сделает это за вас:
const mergeConstrained: <C>() =>
<A extends object, B extends object, CC=Merged<A, B>>(
a: A,
b: B amp; ([CC] extends [C] ? B : C)
) => { [K in keyof CC]: CC[K] }
= () => (a, b) => ({ ...a, ...b }) as any;
В конечном итоге результат сохраняется Merged<A, B>
в параметре типа CC
и возвращается сопоставленный тип, чтобы разделить его. Пользователи будут довольны этой версией, даже несмотря на то, что она более уродлива для разработчика.
О, и да, в реализации mergeConstrained()
используется утверждение типа, потому что мало шансов, что компилятор сможет понять такие манипуляции с универсальными типами.
Давайте посмотрим, работает ли это так, как вы хотите:
type C = { a: string, b: string, c: number };
const mergeC = mergeConstrained<C>();
// normal use
const okay = mergeC({ a: "" }, { b: "", c: 0 }); // okay
// wrong property types
const wrongProps = mergeC({}, { a: "a", b: "b", c: "c" }); // error!
// string is not assignable to number --------> ~
// missing properties give an error
const missingProps = mergeC({ a: "" }, { b: "" }); // error!
// missing "a" and "c" --------------> ~~~~~~~~~
// error could be better but at least it warns about "c"
// extra properties are accepted
const extraProp = mergeC({ a: "" }, { b: "", c: 1, d: 3 });
// extraProp is type { a: string; b: string; c: number; d: number; }
// overlapping props are accepted
const overlap = mergeC({ a: "", b: "" }, { b: "", c: 1 }); //okay
// overlapping props where the merged type is okay are accepted
const overlapBadFirst = mergeC({ a: "", b: 123 }, { b: "", c: 1 }); //okay
// overlapping props where the merged type is wrong are an error
const overlapBadSecond = mergeC({ a: "", b: "" }, { b: 123, c: 1 }); //okay
// number is not assignable to string ------------> ~
Такое поведение мне кажется разумным. Вот <a rel="noreferrer noopener nofollow" href="https://www.typescriptlang.org/play//#src=type Omit = Pick<T, Exclude>;
type Defined = T extends undefined ? never : T;
type MergedProperties = {
[K in (keyof T & keyof U)]:
undefined extends T[K]
? Defined
: T[K]
};
type Merged =
Omit & Omit & MergedProperties;
const mergeConstrained: () =>
<A extends object, B extends object, CC=Merged>(
a: A,
b: B & ([CC] extends [C] ? B : C)
) => { [K in keyof CC]: CC[K] }
= () => (a, b) => ({ …a, …b }) as any;
type C = { a: string, b: string, c: number };
const mergeC = mergeConstrained();
// normal use
const okay = mergeC({ a: «» }, { b: «», c: 0 }); // okay
// wrong property types
const wrongProps = mergeC({}, { a: «a», b: «b», c: «c» }); // error!
// string is not assignable to number ———> ~
// missing properties give an error
const missingProps = mergeC({ a: «» }, { b: «» }); // error!
// missing «a» and «c» —————> ~~~~~~~~~
// error could be better but at least it warns about «c»
// extra properties are accepted
const extraProp = mergeC({ a: «» }, { b: «», c: 1, d: 3 });
// extraProp is type { a: string; b: string; c: number; d: number; }
// overlapping props are accepted
const overlap = mergeC({ a: «», b: «» }, { b: «», c: 1 }); //okay
// overlapping props where the merged type is okay are accepted
const overlapBadFirst = mergeC({ a: «», b: 123 }, { b: «», c: 1 }); //okay
// overlapping props where the merged type is wrong are an error
const overlapBadSecond = mergeC({ a: «», b: «» }, { b: 123, c: 1 }); //okay
// number is not assignable to string ————> ~» rel=»nofollow noreferrer»>ссылка на игровую площадку на приведенный выше код. Надеюсь, это поможет; удачи!
Комментарии:
1. Спасибо за этот ответ, это показывает действительно отличную технику с дополнительным производным универсальным типом. У меня только один вопрос: как это называется и что это делает:
[CC] extends [C]
? Почему бы просто неCC extends C
? Кстати: я действительно ценю ваши два примера, хотя интересно, что первый из них оценивается как «более простые» типы в IntelliJ, но не второй. (Это просто замечание, сейчас это не имеет значения.)2. Я делаю это
[CC] extends [C]
для предотвращения распространения условного типа. Это имеет значение только тогда,CC
когда это тип объединения.3. Спасибо, что написали, помогая мне