Русский
Русский
English
Статистика
Реклама

Перевод Typescript Объединение типов в глубину

Пошаговое руководство о том, как в TypeScript написать такой generic-тип, который объединяет произвольные вложенные key-value структуры.

Примечание переводчика: я намерено не стал переводить некоторые слова (вроде generic, key-value), т.к., на мой взгляд, это только усложнит понимание материала.

TLDR:

Исходный код для DeepMergeTwoTypes будет в конце статьи. Скопируйте его в вашу IDE, чтобы поиграть с ним.

Как это выглядит в vsCode:

Если вы не уверены в своих познаниях о том, как работают generic-и в TypeScript, вы можете ознакомиться с этой статьёй (Mniminalist 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 & Bconst 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-типа DeepMergeTwoTypes

DeepMergeTwoTypes<>

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.

Но нашDeepMergeTwoTypesgeneric не работает рекурсивно со вложенными структурами. Так что давайте вынесем объединение объектов в новый 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
К списку статей
Опубликовано: 08.11.2020 20:23:06
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Typescript

Type inference

Generics

Types

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru