В текущей версии TS (5.7) нет нативного расширения типов.
Расширение в TS реализуют интерфейсы через ключевое слово extend, причем интерфейсы могут быть расширены только от одного объекта.
Хотя для типов (type/interface) предусмотрена операция интерсекции (&), которая объединяет свойства двух или более типов - она обладает важным ограничивающим свойством - при наличии одинаковых свойств, операция интерсекции присваивает их результирующему типу значение never.
Если же мы хотим, чтобы свойство перезаписывалось последним значением пересечения, мы можем написать утилитарную функцию Extend, которая примет в себя массив типов, объединит их, а при наличии свойств с одинаковым именем, запишет в конечное значение последнее из них.
Тип ExtendType будет выглядеть так:
type ExtendType<A extends any[]> = A extends [infer T1, infer T2, ...infer R] ? Omit<T1, keyof T2> & T2 & ExtendType<R> : unknown;
Давайте разберем этот тип.
Так ExtendType принимает A массив типов:
type ExtendType<A extends any[]>
дальше, разбивает его на типы T1, T2 и R:
type ExtendType<A extends any[]> = A extends [infer T1, infer T2, ...infer R]
затем, удаляет из T1 все одинаковые с T2 ключи и возвращет unknown, если на вход не был получен массив:
type ExtendType<A extends any[]> = A extends [infer T1, infer T2, ...infer R] ? Omit<T1, keyof T2> : unknown
после, соединяет результат операции удаления с T2, таким образом, по сути, перезатирая ключи T1 ключами T2:
type ExtendType<A extends any[]> = A extends [infer T1, infer T2, ...infer R] ? Omit<T1, keyof T2> & T2 : unknown
Наконец, выше описанная операция повторяется рекурсивно:
type ExtendType<A extends any[]> = A extends [infer T1, infer T2, ...infer R] ? Omit<T1, keyof T2> & T2 & ExtendType<R> : unknown;
Также для чистого вывода при навередении на тип в IDE, мы можем написать еще одну утилитарную функцию - Pretty, которая линейно переберет все ключи получившегося типа с присвоенными значениями:
export type Pretty<T> = { [K in keyof T]: T[K]; } & {};
По сути, результат типа Extend никак не измениться, но так будет проще читать тип. В итоге, экспортируемый тип Extend будет выглядеть так:
export type Extend<A extends any[]> = Pretty<ExtendType<A>>;
К примеру, так создание типа SuperUser, объединенного из типов:
type User = {
id: number;
name: string;
}
type SuperData = {
token: string;
fullAccess: boolean;
}
будет выглядеть так:
type SuperUser = Extend<[User, SuperData]>;
Так в Extend можно передать любое количество типов, и, если нужно, добавить и перезаписать что-то, то можно просто передать еще тип в функцию:
type SuperUser = Extend<[User, SuperData, {
id: number | string;
fullAccess?: boolean;
}]>;
Полученный в результате тип SuperUser будет таким:
id: number | string;
name: string;
token: string;
fullAccess?: boolean;
Также, если вы используете VSCode - вы можете добавть код обьявления типа Extend в сниппет в файле typescript-а:
и добавить следующий json-сниппет:
"UTILS TYPE EXTEND PRETTY": {
"prefix": "utep",
"body": [
"${1:export }type Pretty<T> = { [K in keyof T]: T[K]; } & {};",
"",
"type ExtendType<A extends any[]> = A extends [infer T1, infer T2, ...infer R] ? Omit<T1, keyof T2> & T2 & ExtendType<R> : unknown;${0}",
"",
"${1:export }type Extend<A extends any[]> = Pretty<ExtendType<A>>;",
"${0}"
]
},
Так при сочентании клавишь ute в .ts файле у вас будет готовая утилита слияния типо Extend.
Комментарии (3)
Alexandroppolus
16.12.2024 21:31Цикл по кортежу, увы, неправильный, там для
[A, B, C, D]
получаетсяExtend<A, B> & Extend<B, D>
, а для[A, B, C]
вообще С проигнорируется. Надо поштучно:type ExtendType<A extends unknown[], R = unknown> = A extends [infer F, ...infer T] ? ExtendType<T, Omit<R, keyof F> & F> : R;
Ну и надо заметить, что это именно extend, а не merge, то есть не всегда то, что может получиться в результате
Object.assign({}, a, b, c)
или{...a, ...b, ...c}
, там всё более хитро, если дело касается опциональных ключей или ключей-индексов.Вышеупомянутый Merge из type-fest тоже по факту работает как extend, но с некоторыми отличиями и менее правильно, например
Merge<{a: number}, {[p: string]: boolean;}>
оставляет ключ "a", хотя все ключи надо заменить на booleanNN1
16.12.2024 21:31Более правильно или менее правильно зависит от желаемого поведения.
Если есть необходимость в другом поведении, просто можно добавить эту функциональсноть в TypeFest.
NN1
Похоже на Merge из Type Fest