[TypeScript] Typescript: Объединение типов в глубину (перевод)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 8 месяцев
Сообщений: 27286
Пошаговое руководство о том, как в TypeScript написать такой generic-тип, который объединяет произвольные вложенные key-value структуры.Примечание переводчика: я намерено не стал переводить некоторые слова (вроде generic, key-value), т.к., на мой взгляд, это только усложнит понимание материала.TLDR:Исходный код для DeepMergeTwoTypes будет в конце статьи. Скопируйте его в вашу IDE, чтобы поиграть с ним.Как это выглядит в vsCode:
Если вы не уверены в своих познаниях о том, как работают generic-и в TypeScript, вы можете ознакомиться с этой статьёй (Miniminalist Typescript - Generics)Если вы хотите проверить корректность кода просто скопируйте его в вашу IDE (прим. переводчика: или в TypeScript Playground песочницу).DisclaimerИспользуя код из этой статьи в production вы делаете это на свой страх и риск (тем не менее, мы его используем).Проблема поведения &-оператора в TypescriptДля начала посмотрим на проблему объединения типов. Определим два типа A и B и новый тип C, который является результатом объединения A & B
type A = { key1: string, key2: string }
type B = { key2: string, key3: string }
type C = A & B
const a = (c: C) => c.
Всё выглядит замечательно до тех пор, пока вы не начнёте объединять несовместимые типы данных.
type A = { key1: string, key2: string }
type B = { key2: null, key3: string }
type C = A & B
Тип A определяет key2 как строку, в то время как в типе B это null.
Typescript выводит это объединение несовместимых типов как never и тип C просто перестаёт работать. В то время как мы ожидали чего-то вроде этого:
type ExpectedType = {
key1: string | null,
key2: string,
key3: string
}
Пошаговое решениеДавайте начнём с создания generic-типа, который будет рекурсивно объединять типы Typescript. Для начала мы определим 2 вспомогательных generic-типа.GetObjDifferentKeys<>
type GetObjDifferentKeys<T, U> = Omit<T, keyof U> & Omit<U, keyof T>
Этот тип принимает на входе 2 объекта и возвращает новый объект, содержащий только уникальные ключи из A и B.
type A = { key1: string, key2: string }
type B = { key2: null, key3: string }
type C = GetObjDifferentKeys<A, B>['']
GetObjSameKeys<>В противовес предыдущему generic-у объявим другой тип, который вытащит все ключи, которые есть в обоих объектах.
type GetObjSameKeys<T, U> = Omit<T | U, keyof GetObjDifferentKeys<T, U>>
Возвращаемый тип — объект.
type A = { key1: string, key2: string }
type B = { key2: null, key3: string }
type C = GetObjSameKeys<A, B>
Все вспомогательные типы готовы, так что мы можем приступать к реализации нашего главного generic-типа DeepMergeTwoTypesDeepMergeTwoTypes<>
type DeepMergeTwoTypes<T, U> =
// "не общие" (уникальные) ключи - опциональны
Partial<GetObjDifferentKeys<T, U>>
// общие ключи - обязательны
& { [K in keyof GetObjSameKeys<T, U>]: T[K] | U[K] }
Этот generic находит все "не общие" ключи между объектами T и U, и сделает их опциональными (необязательными). Спасибо за это стандартному типу Partial<>, из стандартной библиотеки типов Typescript. Этот тип с опциональными ключами объединяется (посредством &-оператора) с объектом содержащим все общие ключи между T и U , значением которых будут T[K] | U[K].Посмотрите на пример ниже. Новый generic нашёл "не-общие" ключи и сделал их опциональными (?), в то время как остальные ключи строго обязательны.
type A = { key1: string, key2: string }
type B = { key2: null, key3: string }
const fn = (c: DeepMergeTwoTypes<A, B>) => c.
Но наш DeepMergeTwoTypes generic не работает рекурсивно со вложенными структурами. Так что давайте вынесем объединение объектов в новый generic тип MergeTwoObjects и будем вызывать DeepMergeTwoTypes рекурсивно до тех пор, пока он не объединит все вложенные структуры.
// этот generic рекурсивно вызывает DeepMergeTwoTypes<>
type MergeTwoObjects<T, U> =
// "не общие" (уникальные) ключи - опциональны
Partial<GetObjDifferentKeys<T, U>>
// общие ключи - обязательны
& {[K in keyof GetObjSameKeys<T, U>]: DeepMergeTwoTypes<T[K], U[K]>}
export type DeepMergeTwoTypes<T, U> =
// проверяем являются ли типы массивами, распаковываем и запускаем рекурсию
[T, U] extends [{ [key: string]: unknown }, { [key: string]: unknown } ]
? MergeTwoObjects<T, U>
: T | U
PRO TIP: Обратите внимание на то, что в DeepMergeTwoTypes используется if-else условие (extends ?:) Мы проверяем что и T и U удовлетворяют условию, засунув их в кортеж (tuple) [T, U]. Это поведение похоже на &&-оператор в Javascript.Этот generic проверяет, что оба параметра соответствуют типу { [key: string]: unknown } (это Object). Если это так, то он объединяет их посредством MergeTwoObject<>. Этот процесс рекурсивно повторяется для всех вложенных объектов.Примечание переводчика: Проверка на extends { [key: string]: unknown } позволяет отфильтровать все не-объекты, т.е. строки, числа, booleans и т.д.. И вуаля! Теперь наш generic рекурсивно применён ко всем вложенным объектам. Пример:
type A = { key: { a: null, c: string} }
type B = { key: { a: string, b: string} }
const fn = (c: MergeTwoObjects<A, B>) => c.key.
На этом всё?Увы, нет. Наш новый generic не поддерживает массивы.Прежде, чем мы продолжим, мы должны понять ключевое слово infer (to infer - выводить).infer смотрит на структуру данных и вытаскивает её тип (в нашем случае это массив). Подробнее почитать про infer можно здесь (Type inference in conditional types).Пример использования infer. Здесь мы получаем тип отдельно взятого элемента массива (Item):
export type ArrayElement<A> = A extends (infer T)[] ? T : never
// Item === (number | string)
type Item = ArrayElement<(number | string)[]>
Теперь мы можем добавить поддержку массивов, просто добавив эти две строки, в которых мы выводим тип значений элементов массива. И рекурсивно вызываем DeepMergeTwoTypes для содержимого массивов.
export type DeepMergeTwoTypes<T, U> =
// ----- 2 добавленные строки ------
// эта ⏬
[T, U] extends [(infer TItem)[], (infer UItem)[]]
// ... и эта ⏬
? DeepMergeTwoTypes<TItem, UItem>[]
: ... rest of previous generic ...
Сейчас DeepMergeTwoTypes может рекурсивно вызывать сам себя, в случае если значения это объекты или массивы.
type A = [{ key1: string, key2: string }]
type B = [{ key2: null, key3: string }]
const fn = (c: DeepMergeTwoTypes<A, B>) => c[0].
И это работает! На этом всё?Эх... Нет. Последняя проблема заключается в объединении Nullable типов с non-nullable.
type A = { key1: string }
type B = { key1: undefined }
type C = DeepMergeTwoTypes<A, B>['key']
Ожидаемый тип — string | undefined, но на деле это не так. Давайте добавим ещё две строки в нашу цепочку if-else .
export type DeepMergeTwoTypes<T, U> =
[T, U] extends [(infer TItem)[], (infer UItem)[]]
? DeepMergeTwoTypes<TItem, UItem>[]
: [T, U] extends [{ [key: string]: unknown}, { [key: string]: unknown } ]
? MergeTwoObjects<T, U>
// ----- 2 добавленные строки ------
// эта ⏬
: [T, U] extends [
{ [key: string]: unknown } | undefined,
{ [key: string]: unknown } | undefined
]
// ... и эта ⏬
? MergeTwoObjects<NonNullable<T>, NonNullable<U>> | undefined
: T | U
Проверяем объединение nullable значений:
type A = { key1: string }
type B = { key1: undefined }
const fn = (c: DeepMergeTwoTypes<A, B>) => c.key1;
И... Вот теперь всё!Мы сделали это! Значения корректно объединяются даже для nullable , вложенных объектов и массивов.Давайте опробуем наш generic на более сложных данных:
type A = { key1: { a: { b: 'c'} }, key2: undefined }
type B = { key1: { a: {} }, key3: string }
const fn = (c: DeepMergeTwoTypes<A, B>) => c.
Полный исходный код:
/**
* Принимает 2 объекта T и U и создаёт новый объект, с их уникальными
* ключами. Используется в `DeepMergeTwoTypes`
*/
type GetObjDifferentKeys<T, U> = Omit<T, keyof U> & Omit<U, keyof T>
/**
* Принимает 2 объекта T and U и создаёт новый объект с их ключами
* Используется в `DeepMergeTwoTypes`
*/
type GetObjSameKeys<T, U> = Omit<T | U, keyof GetObjDifferentKeys<T, U>>
type MergeTwoObjects<T, U> =
// "не общие" ключи опциональны
Partial<GetObjDifferentKeys<T, U>>
// общие ключи рекурсивно заполняются за счёт `DeepMergeTwoTypes<...>`
& { [K in keyof GetObjSameKeys<T, U>]: DeepMergeTwoTypes<T[K], U[K]> }
// объединяет 2 типа
export type DeepMergeTwoTypes<T, U> =
// проверяет являются ли типы массивами, распаковывает их и
// запускает рекурсию
[T, U] extends [(infer TItem)[], (infer UItem)[]]
? DeepMergeTwoTypes<TItem, UItem>[]
// если типы это объекты
: [T, U] extends [
{ [key: string]: unknown},
{ [key: string]: unknown }
]
? MergeTwoObjects<T, U>
: [T, U] extends [
{ [key: string]: unknown } | undefined,
{ [key: string]: unknown } | undefined
]
? MergeTwoObjects<NonNullable<T>, NonNullable<U>> | undefined
: T | U
// тестируем:
type A = { key1: { a: { b: 'c'} }, key2: undefined }
type B = { key1: { a: {} }, key3: string }
const fn = (c: DeepMergeTwoTypes<A, B>) => c.key
Последний штрихКак бы так поправить DeepMergeTwoTypes<T, U> generic, чтобы он мог принимать N аргументов вместо двух? Я оставлю этот материал для следующей статьи, но вы можете посмотреть мой рабочий черновик здесь).
Примечание переводчикаЭто мой первый опыт перевода. Убедительная просьба об опечатках, запятых и просто косноязычных фразах писать в личку.
===========
Источник:
habr.com
===========
===========
Автор оригинала: Jakub Švehla
===========Похожие новости:
- [VueJS, TypeScript] vuex + typescript = vuexok. Велосипед, который поехал и обогнал всех
- [Habr, ReactJS, TypeScript] Мама, я сделал Хабр
- [Программирование, Функциональное программирование, TypeScript] Функциональное программирование на TypeScript: полиморфизм родов высших порядков
- [JavaScript, Angular, ReactJS, VueJS, TypeScript] Создание микросервисной архитектуры с использованием single-spa (миграция существующего проекта)
- [Разработка веб-сайтов, JavaScript, Angular, TypeScript] Давайте сделаем переиспользуемый компонент tree view в Angular
- [JavaScript, Разработка игр, TypeScript, Логические игры] DagazServer: Как всё устроено
- [JavaScript, Программирование, Java, TypeScript] TypeScript для бэкенд-разработки (перевод)
- [Разработка веб-сайтов, CSS, TypeScript] Продвинутый CSS-in-TS
- [Администрирование баз данных, SQL, PostgreSQL, MySQL] Восемь интересных возможностей PostgreSQL, о которых вы, возможно, не знали (перевод)
- [Программирование, TypeScript] Chorda 2.0. Как я потерял и нашел API
Теги для поиска: #_typescript, #_type_inference, #_generics, #_types, #_typescript
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 01-Ноя 09:45
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 8 месяцев |
|
Пошаговое руководство о том, как в TypeScript написать такой generic-тип, который объединяет произвольные вложенные key-value структуры.Примечание переводчика: я намерено не стал переводить некоторые слова (вроде generic, key-value), т.к., на мой взгляд, это только усложнит понимание материала.TLDR:Исходный код для DeepMergeTwoTypes будет в конце статьи. Скопируйте его в вашу IDE, чтобы поиграть с ним.Как это выглядит в vsCode: Если вы не уверены в своих познаниях о том, как работают generic-и в TypeScript, вы можете ознакомиться с этой статьёй (Miniminalist Typescript - Generics)Если вы хотите проверить корректность кода просто скопируйте его в вашу IDE (прим. переводчика: или в TypeScript Playground песочницу).DisclaimerИспользуя код из этой статьи в production вы делаете это на свой страх и риск (тем не менее, мы его используем).Проблема поведения &-оператора в TypescriptДля начала посмотрим на проблему объединения типов. Определим два типа A и B и новый тип C, который является результатом объединения A & B type A = { key1: string, key2: string }
type B = { key2: string, key3: string } type C = A & B const a = (c: C) => c. Всё выглядит замечательно до тех пор, пока вы не начнёте объединять несовместимые типы данных. type A = { key1: string, key2: string }
type B = { key2: null, key3: string } type C = A & B Typescript выводит это объединение несовместимых типов как never и тип C просто перестаёт работать. В то время как мы ожидали чего-то вроде этого: type ExpectedType = {
key1: string | null, key2: string, key3: string } type GetObjDifferentKeys<T, U> = Omit<T, keyof U> & Omit<U, keyof T>
type A = { key1: string, key2: string }
type B = { key2: null, key3: string } type C = GetObjDifferentKeys<A, B>[''] GetObjSameKeys<>В противовес предыдущему generic-у объявим другой тип, который вытащит все ключи, которые есть в обоих объектах. type GetObjSameKeys<T, U> = Omit<T | U, keyof GetObjDifferentKeys<T, U>>
type A = { key1: string, key2: string }
type B = { key2: null, key3: string } type C = GetObjSameKeys<A, B> Все вспомогательные типы готовы, так что мы можем приступать к реализации нашего главного generic-типа DeepMergeTwoTypesDeepMergeTwoTypes<> type DeepMergeTwoTypes<T, U> =
// "не общие" (уникальные) ключи - опциональны Partial<GetObjDifferentKeys<T, U>> // общие ключи - обязательны & { [K in keyof GetObjSameKeys<T, U>]: T[K] | U[K] } type A = { key1: string, key2: string }
type B = { key2: null, key3: string } const fn = (c: DeepMergeTwoTypes<A, B>) => c. Но наш DeepMergeTwoTypes generic не работает рекурсивно со вложенными структурами. Так что давайте вынесем объединение объектов в новый generic тип MergeTwoObjects и будем вызывать DeepMergeTwoTypes рекурсивно до тех пор, пока он не объединит все вложенные структуры. // этот generic рекурсивно вызывает DeepMergeTwoTypes<>
type MergeTwoObjects<T, U> = // "не общие" (уникальные) ключи - опциональны Partial<GetObjDifferentKeys<T, U>> // общие ключи - обязательны & {[K in keyof GetObjSameKeys<T, U>]: DeepMergeTwoTypes<T[K], U[K]>} export type DeepMergeTwoTypes<T, U> = // проверяем являются ли типы массивами, распаковываем и запускаем рекурсию [T, U] extends [{ [key: string]: unknown }, { [key: string]: unknown } ] ? MergeTwoObjects<T, U> : T | U type A = { key: { a: null, c: string} }
type B = { key: { a: string, b: string} } const fn = (c: MergeTwoObjects<A, B>) => c.key. На этом всё?Увы, нет. Наш новый generic не поддерживает массивы.Прежде, чем мы продолжим, мы должны понять ключевое слово infer (to infer - выводить).infer смотрит на структуру данных и вытаскивает её тип (в нашем случае это массив). Подробнее почитать про infer можно здесь (Type inference in conditional types).Пример использования infer. Здесь мы получаем тип отдельно взятого элемента массива (Item): export type ArrayElement<A> = A extends (infer T)[] ? T : never
// Item === (number | string) type Item = ArrayElement<(number | string)[]> export type DeepMergeTwoTypes<T, U> =
// ----- 2 добавленные строки ------ // эта ⏬ [T, U] extends [(infer TItem)[], (infer UItem)[]] // ... и эта ⏬ ? DeepMergeTwoTypes<TItem, UItem>[] : ... rest of previous generic ... type A = [{ key1: string, key2: string }]
type B = [{ key2: null, key3: string }] const fn = (c: DeepMergeTwoTypes<A, B>) => c[0]. И это работает! На этом всё?Эх... Нет. Последняя проблема заключается в объединении Nullable типов с non-nullable. type A = { key1: string }
type B = { key1: undefined } type C = DeepMergeTwoTypes<A, B>['key'] Ожидаемый тип — string | undefined, но на деле это не так. Давайте добавим ещё две строки в нашу цепочку if-else . export type DeepMergeTwoTypes<T, U> =
[T, U] extends [(infer TItem)[], (infer UItem)[]] ? DeepMergeTwoTypes<TItem, UItem>[] : [T, U] extends [{ [key: string]: unknown}, { [key: string]: unknown } ] ? MergeTwoObjects<T, U> // ----- 2 добавленные строки ------ // эта ⏬ : [T, U] extends [ { [key: string]: unknown } | undefined, { [key: string]: unknown } | undefined ] // ... и эта ⏬ ? MergeTwoObjects<NonNullable<T>, NonNullable<U>> | undefined : T | U type A = { key1: string }
type B = { key1: undefined } const fn = (c: DeepMergeTwoTypes<A, B>) => c.key1; И... Вот теперь всё!Мы сделали это! Значения корректно объединяются даже для nullable , вложенных объектов и массивов.Давайте опробуем наш generic на более сложных данных: type A = { key1: { a: { b: 'c'} }, key2: undefined }
type B = { key1: { a: {} }, key3: string } const fn = (c: DeepMergeTwoTypes<A, B>) => c. Полный исходный код: /**
* Принимает 2 объекта T и U и создаёт новый объект, с их уникальными * ключами. Используется в `DeepMergeTwoTypes` */ type GetObjDifferentKeys<T, U> = Omit<T, keyof U> & Omit<U, keyof T> /** * Принимает 2 объекта T and U и создаёт новый объект с их ключами * Используется в `DeepMergeTwoTypes` */ type GetObjSameKeys<T, U> = Omit<T | U, keyof GetObjDifferentKeys<T, U>> type MergeTwoObjects<T, U> = // "не общие" ключи опциональны Partial<GetObjDifferentKeys<T, U>> // общие ключи рекурсивно заполняются за счёт `DeepMergeTwoTypes<...>` & { [K in keyof GetObjSameKeys<T, U>]: DeepMergeTwoTypes<T[K], U[K]> } // объединяет 2 типа export type DeepMergeTwoTypes<T, U> = // проверяет являются ли типы массивами, распаковывает их и // запускает рекурсию [T, U] extends [(infer TItem)[], (infer UItem)[]] ? DeepMergeTwoTypes<TItem, UItem>[] // если типы это объекты : [T, U] extends [ { [key: string]: unknown}, { [key: string]: unknown } ] ? MergeTwoObjects<T, U> : [T, U] extends [ { [key: string]: unknown } | undefined, { [key: string]: unknown } | undefined ] ? MergeTwoObjects<NonNullable<T>, NonNullable<U>> | undefined : T | U // тестируем: type A = { key1: { a: { b: 'c'} }, key2: undefined } type B = { key1: { a: {} }, key3: string } const fn = (c: DeepMergeTwoTypes<A, B>) => c.key Примечание переводчикаЭто мой первый опыт перевода. Убедительная просьба об опечатках, запятых и просто косноязычных фразах писать в личку. =========== Источник: habr.com =========== =========== Автор оригинала: Jakub Švehla ===========Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 01-Ноя 09:45
Часовой пояс: UTC + 5