Ts Masters: 22 примера, подробно объясняющих самые малоизвестные продвинутые инструменты Ts

внешний интерфейс TypeScript
Ts Masters: 22 примера, подробно объясняющих самые малоизвестные продвинутые инструменты Ts

Всем привет, я Ленг Хаммер. Я считаю, что с неудержимой тенденцией Typescript мои друзья использовали T для разработки более или менее. В дополнение к базовым определениям типов, использование Ts относительно незнакомо с обобщениями, встроенными расширенными типами и пользовательскими инструментами расширенного типа Ts. В этой статье подробно объясняются принципы и навыки программирования инструментов типа T с помощью примеров из 22 типов инструментов. Не надо говорить о праздных статьях, весь процесс сухой, а контента много.Для тех, кто хочет улучшить навыки ТС, терпеливо читайте. Я верю, что прочитав эту статью, вы сможете глубже разобраться в этой теме. Теперь приступим~

Эта статья в основном разделена на три части:

  • В первой части объясняются некоторые основные функции ключевых слов (такие как запрос индекса, доступ к индексу, сопоставление,extendsд.), но в этой части будут объясняться некоторые непонятные друзьям особенности, а основные функции повторяться не будут. Дополнительные ключевые слова и методы будут включены в последующие демонстрации примеров, а затем подробно описаны;
  • Во второй части объясняются встроенные инструменты типов и принципы реализации Т, такие какPick,OmitЖдать;
  • Третья часть объясняет пользовательский тип инструмента.Эта часть также является самой сложной частью.Он будет постепенно анализироваться с помощью некоторых примеров инструментов сложного типа, а неясные места и задействованные точки знаний будут объясняться шаг за шагом. Эта часть также будет включать в себя множество навыков программирования для инструментов типа T, и я надеюсь, что благодаря объяснению этой части навыки Ts моих друзей могут быть улучшены!

Первая часть преамбулы

  • keyofИндексный запрос

для любого типаT,keyof TРезультатом является объединение всех ключей открытых свойств по типу:

interface Eg1 {
  name: string,
  readonly age: number,
}
// T1的类型实则是name | age
type T1 = keyof Eg1

class Eg2 {
  private name: string;
  public readonly age: number;
  protected home: string;
}
// T2实则被约束为 age
// 而name和home不是公有属性,所以不能被keyof获取到
type T2 = keyof Eg2
  • T[K]доступ к индексу
interface Eg1 {
  name: string,
  readonly age: number,
}
// string
type V1 = Eg1['name']
// string | number
type V2 = Eg1['name' | 'age']
// any
type V2 = Eg1['name' | 'age2222']
// string | number
type V3 = Eg1[keyof Eg1]

T[keyof T]способ получитьTвсеkeyТип объединения, состоящий из типов;T[keyof K]образом, полученноеTсерединаkeyи существовать одновременноKТип союза, состоящий из типов времени; Примечание: если[]Если ключ в T не существует в T, то он любой, поскольку ts не знает, какого типа ключ в конце концов, он любой, и он также сообщит об ошибке;

  • &Важные моменты о типах кроссовера

Объединение нескольких типов, взятых пересекающимися типами, но если одинаковыеkeyно разные типы, т.keyзаnever.

interface Eg1 {
  name: string,
  age: number,
}

interface Eg2 {
  color: string,
  age: string,
}

/**
 * T的类型为 {name: string; age: never; color: string}
 * 注意,age因为Eg1和Eg2中的类型不一致,所以交叉后age的类型是never
 */
type T = Eg1 & Eg2
// 可通过如下示例验证
const val: T = {
  name: '',
  color: '',
  age: (function a() {
    throw Error()
  })(),
}

расширяет функцию ключевого слова (выделение)

  • Используется для интерфейсов, указывающих на наследование
interface T1 {
  name: string,
}

interface T2 {
  sex: number,
}

/**
 * @example
 * T3 = {name: string, sex: number, age: number}
 */
interface T3 extends T1, T2 {
  age: number,
}

Обратите внимание, что интерфейсы поддерживают множественное наследование, а синтаксис разделен запятыми. Если тип реализует наследование, вы можете использовать перекрестный типtype A = B & C & D.

  • Указывает тип условия, который можно использовать для оценки состояния.

Обозначает условное суждение.Если предыдущее условие выполнено, возвращается первый параметр после вопросительного знака, иначе второй. Подобно тернарной операции js.

/**
 * @example
 * type A1 = 1
 */
type A1 = 'x' extends 'x' ? 1 : 2;

/**
 * @example
 * type A2 = 2
 */
type A2 = 'x' | 'y' extends 'x' ? 1 : 2;

/**
 * @example
 * type A3 = 1 | 2
 */
type P<T> = T extends 'x' ? 1 : 2;
type A3 = P<'x' | 'y'>

Спроси почемуA2иA3значение отличается?

  • Если он используется для простого условного суждения, то он предназначен для непосредственного суждения о том, можно ли отнести предыдущий тип к последнему типу.
  • какextendsЕсли предыдущий тип является универсальным типом, а универсальный тип передается как тип объединения, в свою очередь будет оцениваться, могут ли все подтипы типа объединения быть присвоены типу после расширения (это процесс распределения).

Подводя итог, этоextendsКогда предыдущий параметр является типом объединения, он будет декомпозирован (обход всех подтипов по очереди для условной оценки) для оценки типа объединения. Конечный результат затем объединяется в новый тип объединения.

  • Предотвратить ключевое слово extends для функции распределения типов объединения

Если вы не хотите, чтобы вас разлагали (распределяли), подход также очень прост, вы можете обернуть следующее в простой тип кортежа:

type P<T> = [T] extends ['x'] ? 1 : 2;
/**
 * type A4 = 2;
 */
type A4 = P<'x' | 'y'>

Документация по распределенным функциям для условных типов

совместимость типов

В теории множеств, если все элементы множества существуют в множестве B, то A является подмножеством B;

В системе типов тип является подтипом, если его свойства более специфичны. (Поскольку атрибутов меньше, это означает, что ограничение типа является более общим и является родительским типом)

Поэтому можно сделать основной вывод: подтипы более специфичны, чем супертипы, а супертипы более общие, чем подтипы.Ниже мы также дополнительно объясним на основе воспроизводимости (присваиваемости), ковариантности, контравариантности и двунаправленной ковариантности типов.

  • уступчивость
interface Animal {
  name: string;
}

interface Dog extends Animal {
  break(): void;
}

let a: Animal;
let b: Dog;

// 可以赋值,子类型更佳具体,可以赋值给更佳宽泛的父类型
a = b;
// 反过来不行
b = a;
  • Свойства присваиваемости в типах объединения
type A = 1 | 2 | 3;
type B = 2 | 3;
let a: A;
let b: B;

// 不可赋值
b = a;
// 可以赋值
a = b;

Да или нетAбольше типов,AЭто подтип? Напротив,AЗдесь больше типов, но типы, которые они выражают, шире, поэтомуAявляется родительским типом,Bявляется подтипом.

следовательноb = aне выполняется (родительский тип не может быть назначен дочернему типу), иa = bустановлены (подтипы могут быть отнесены к супертипам).

  • ковариантный
interface Animal {
  name: string;
}

interface Dog extends Animal {
  break(): void;
}

let Eg1: Animal;
let Eg2: Dog;
// 兼容,可以赋值
Eg1 = Eg2;

let Eg3: Array<Animal>
let Eg4: Array<Dog>
// 兼容,可以赋值
Eg3 = Eg4

пройти черезEg3иEg4заглянутьAnimalиDogПревратившись в массив,Array<Dog>еще можно назначитьArray<Animal>, Таким образом, дляtype MakeArray = Array<any>Это ковариантно.

Наконец, цитируя определение в Википедии:

Ковариация и контравариантность (Ковариантность и контравариантность) в информатике для описания того, есть ли родитель/потомок среди нескольких сложных типов, созданных с помощью конструктора типов для нескольких типов с отношением типа родитель/потомок.Термин отношения типа.

Проще говоря, несколько типов с отношением родитель-потомок, в новом типе, построенном через определенное структурное отношение, если они также имеют отношение родитель-потомок, то они ковариантны, и отношения обратные (потомок становится родителем, родитель становится дочерним). ) инвертируется. Это может звучать абстрактно, но ниже мы продемонстрируем это на более конкретном примере:

  • Инверсия
interface Animal {
  name: string;
}

interface Dog extends Animal {
  break(): void;
}

type AnimalFn = (arg: Animal) => void
type DogFn = (arg: Dog) => void

let Eg1: AnimalFn;
let Eg2: DogFn;
// 不再可以赋值了,
// AnimalFn = DogFn不可以赋值了, Animal = Dog是可以的
Eg1 = Eg2;
// 反过来可以
Eg2 = Eg1;

В теории,Animal = Dogявляется типобезопасным, тоAnimalFn = DogFnОн также должен быть типобезопасным, почему Ts считает, что это небезопасно? См. пример ниже:

let animal: AnimalFn = (arg: Animal) => {}
let dog: DogFn = (arg: Dog) => {
  arg.break();
}

// 假设类型安全可以赋值
animal = dog;
// 那么animal在调用时约束的参数,缺少dog所需的参数,此时会导致错误
animal({name: 'cat'});

В этом примере, если функция собаки назначена функции животного, то при вызове функции животного ограничение состоит в том, что параметр должен быть типа Животное (а не Собака), но на самом деле животное вызывается собакой. и в это время будет ошибка.

следовательно,AnimalиDogв ходе выполненияtype Fn<T> = (arg: T) => voidПосле создания конструктора отношения родитель-потомок меняются местами, что называется «контравариантностью».

  • двусторонняя ковариация

Фактически стратегия по умолчанию, принятая Ts при сравнении параметров функции, представляет собой двустороннюю ковариацию: присвоение может быть успешным только тогда, когда параметры исходной функции могут быть присвоены целевой функции или наоборот.

Это нестабильно, потому что вызывающая сторона может передать функцию с более точной информацией о типе, но вызвать переданную функцию с менее точной информацией о типе (как правило, контравариантность, описанная выше). Но на практике это редко вызывает ошибки и позволяет использовать многие распространенные шаблоны в JavaScript:

// lib.dom.d.ts中EventListener的接口定义
interface EventListener {
  (evt: Event): void;
}
// 简化后的Event
interface Event {
  readonly target: EventTarget | null;
  preventDefault(): void;
}
// 简化合并后的MouseEvent
interface MouseEvent extends Event {
  readonly x: number;
  readonly y: number;
}

// 简化后的Window接口
interface Window {
  // 简化后的addEventListener
  addEventListener(type: string, listener: EventListener)
}

// 日常使用
window.addEventListener('click', (e: Event) => {});
window.addEventListener('mouseover', (e: MouseEvent) => {});

можно увидетьWindowизlistenerФункция требует, чтобы параметры былиEvent, но в повседневном использовании чаще передается вEventПодтип. Но здесь он отлично работает, поэтому его поведение по умолчанию — двусторонняя ковариация. в состоянии пройтиtsconfig.jsизменено вstrictFunctionTypeсвойства строго контролировать ковариантность и контравариантность.

Попади в точку! ! ! Попади в точку! ! ! Попади в точку! ! !

inferФункция ключевого слова пока не будет объясняться слишком подробно.extendsВ условии тип , пусть Ts подтолкнет его к типу сам по себе, вы можете обратиться к официальному сайту за подробностями. Но оinferНекоторые из легко упускаемых из виду, но очень важных особенностей , должны быть упомянуты здесь:

  • inferВыведенные имена одинаковы, и оба находятся вИнверсия, результатом вывода будетперекрестный тип.
type Bar<T> = T extends {
  a: (x: infer U) => void;
  b: (x: infer U) => void;
} ? U : never;

// type T1 = string
type T1 = Bar<{ a: (x: string) => void; b: (x: string) => void }>;

// type T2 = never
type T2 = Bar<{ a: (x: string) => void; b: (x: number) => void }>;
  • inferВыведенные имена одинаковы, и оба находятся вковариантный, результатом вывода будеттип союза.
type Foo<T> = T extends {
  a: infer U;
  b: infer U;
} ? U : never;

// type T1 = string
type T1 = Foo<{ a: string; b: string }>;

// type T2 = string | number
type T2 = Foo<{ a: string; b: number }>;

Для получения справочных документов по меж- и ковариантной контравариантности нажмите здесь.

企业微信截图_8357a6f0-aa88-4faf-b21e-f1baa6bc790e.png

Вторая часть анализа принципа встроенного инструмента типа Ts

Анализ принципа частичной реализации

Partial<T>будетTВсе свойства становятся необязательными.

/**
 * 核心实现就是通过映射类型遍历T上所有的属性,
 * 然后将每个属性设置为可选属性
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
}
  • [P in keyof T]Перемещение по типу картыTвсе свойства на
  • ?:установить как атрибут необязательно
  • T[P]установить тип на исходный тип

Чтобы расширить, сформулируемkeyстановится необязательным типом:

/**
 * 主要通过K extends keyof T约束K必须为keyof T的子类型
 * keyof T得到的是T的所有key组成的联合类型
 */
type PartialOptional<T, K extends keyof T> = {
  [P in K]?: T[P];
}

/**
 * @example
 *     type Eg1 = { key1?: string; key2?: number }
 */
type Eg1 = PartialOptional<{
  key1: string,
  key2: number,
  key3: ''
}, 'key1' | 'key2'>;

Анализ принципа только для чтения

/**
 * 主要实现是通过映射遍历所有key,
 * 然后给每个key增加一个readonly修饰符
 */
type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

/**
 * @example
 * type Eg = {
 *   readonly key1: string;
 *   readonly key2: number;
 * }
 */
type Eg = Readonly<{
  key1: string,
  key2: number,
}>

Pick

Выбирает набор свойств и составляет новый тип.

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

Основные пункты знаний, которые совпадают с упомянутыми выше, здесь не повторяются.

Record

построитьtype,keyдля каждого подтипа в типе объединения типT. Текст не прост для понимания, давайте сначала посмотрим на пример:

/**
 * @example
 * type Eg1 = {
 *   a: { key1: string; };
 *   b: { key1: string; };
 * }
 * @desc 就是遍历第一个参数'a' | 'b'的每个子类型,然后将值设置为第二参数
 */
type Eg1 = Record<'a' | 'b', {key1: string}>

Запишите конкретную реализацию:

/**
 * 核心实现就是遍历K,将值设置为T
 */
type Record<K extends keyof any, T> = {
  [P in K]: T
}

/**
 * @example
 * type Eg2 = {a: B, b: B}
 */
interface A {
  a: string,
  b: number,
}
interface B {
  key1: number,
  key2: string,
}
type Eg2 = Record<keyof A, B>
  • Стоит отметить, чтоkeyof anyполучить этоstring | number | symbol
  • Причина в том, что тип ключа может быть только типаstring | number | symbol

Расширение: гомоморфизм и негомоморфизм. Фокус! ! ! Фокус! ! ! Фокус! ! !

  • Partial,ReadonlyиPickВсе они гомоморфны, то есть их реализация требует ввода типа T для копирования свойств, поэтому модификаторы свойств (например, только для чтения, ?:) будут скопированы. В этом можно убедиться на следующем примере:
/**
 * @example
 * type Eg = {readonly a?: string}
 */
type Eg = Pick<{readonly a?: string}, 'a'>

отEgВ результате можно увидеть, что когда Pick копирует атрибуты, он также копируетreadonlyи?:модификатор.

  • Recordявляется негомоморфным и не нуждается в копировании свойств, поэтому модификаторы свойств не копируются

Может быть, некоторые друзья здесь задаются вопросом, почему?Pickатрибуты копируются, аRecordнет копии? Сравним его реализацию:

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

type Record<K extends keyof any, T> = {
  [P in K]: T
}

можно увидетьPickреализации, обратите внимание, чтоP in K(по сутиP in keyof T), T — тип ввода, аkeyof Tзатем просматривает типы ввода; иRecordВ реализации он не проходит через все типы входных данных, K ограничивается толькоkeyof anyподтип .

Наконец, давайте сравнимPick、Partial、readonlyЭти типы инструментов, без исключения, используютсяkeyof Tдля помощи в копировании свойств входящего типа.

Исключить принципиальный анализ

Exclude<T, U>Экстракт существует вT, но не существует вUТип объединения типов.

/**
 * 遍历T中的所有子类型,如果该子类型约束于U(存在于U、兼容于U),
 * 则返回never类型,否则返回该子类型
 */
type Exclude<T, U> = T extends U ? never : T;

/**
 * @example
 * type Eg = 'key1'
 */
type Eg = Exclude<'key1' | 'key2', 'key2'>

Попади в точку! ! !

  • neverпредставляет несуществующий тип
  • neverПосле объединения с другими типами нетneverиз
/**
 * @example
 * type Eg2 = string | number
 */
type Eg2 = string | number | never

Поэтому вышеизложенноеEgНа самом деле он равенkey1 | never, это,type Eg = key1

Extract

Extract<T, U>Извлеките все пересечения типа объединения T и типа объединения U.

type Extract<T, U> = T extends U ? T : never;

/**
 * @example
 *  type Eg = 'key1'
 */
type Eg = Extract<'key1' | 'key2', 'key1'>

Анализ принципа пропуска

Omit<T, K>от типаTсредняя выбраковкаKвсе свойства в .

/**
 * 利用Pick实现Omit
 */
type Omit = Pick<T, Exclude<keyof T, K>>;
  • Подумайте об этом по-другому, его реализация может быть использованаPickИзвлеките тип ключей, который нам нужен
  • Поэтому этоOmit = Pick<T, 我们需要的属性联合>
  • И объединение атрибутов, которое нам нужно, состоит в том, чтобы исключить атрибуты, существующие в типе объединения K, из объединения атрибутов T.
  • Поэтому этоExclude<keyof T, K>;

Что, если мы не будем использовать Pick?

/**
 * 利用映射类型Omit
 */
type Omit2<T, K extends keyof any> = {
  [P in Exclude<keyof T, K>]: T[P]
}
  • Его реализация аналогична реализации принципа Пика.
  • Разница в том, что атрибуты, которые нам нужно пройти, разные.
  • Нам нужны те же свойства, что и в примере выше, т.е.Exclude<keyof T, K>
  • Таким образом, обход[P in Exclude<keyof T, K>]

Параметры и возвращаемый тип

Параметры Получает типы параметров функции, помещая каждый тип параметра в кортеж.

/**
 * @desc 具体实现
 */
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

/**
 * @example
 * type Eg = [arg1: string, arg2: number];
 */
type Eg = Parameters<(arg1: string, arg2: number) => void>;
  • ParametersСначала ограничьте параметрыTдолжен быть типом функции, поэтому(...args: any) => any>заменитьFunctionэто тоже хорошо
  • Конкретная реализация - судитьTЯвляется ли это типом функции, если да, используйтеinter PПусть ts сам выводит тип параметра функции и сохраняет выведенный результат в типPвключено, иначе возвратnever;

Попади в точку! ! ! Попади в точку! ! ! Попади в точку! ! !

  • inferФункция ключевого слова состоит в том, чтобы позволить Ts вывести тип самостоятельно и сохранить результат вывода в типе его привязки параметра. Например:infer Pзаключается в сохранении результата в видеPна, для использования.
  • inferКлючевые слова могут быть только вextendsИспользуется в условных типах и не может использоваться где-либо еще.

Еще раз в точку! ! ! Еще раз в точку! ! ! Еще раз в точку! ! !

  • type Eg = [arg1: string, arg2: number]Это кортеж, но такой же, как наш общий кортежtype tuple = [string, number]. На официальном сайте эта часть документации не упоминается, по сути под этим можно понимать похожий именованный кортеж, или значение именованного кортежа. По сути, никакой специальной функции нет, например, значение нельзя получить через этот именованный. Но с семантической точки зрения лично мне кажется, что семантических выражений больше.

  • Необязательные параметры, определяющие кортеж, только последний параметр

/**
 * 普通方式
 */
type Tuple1 = [string, number?];
const a: Tuple1 = ['aa', 11];
const a2: Tuple1 = ['aa'];

/**
 * 具名方式
 */
type Tuple2 = [name: string, age?: number];
const b: Tuple2 = ['aa', 11];
const b2: Tuple2 = ['aa'];

Расширение:inferРеализуйте выведенный тип для всех элементов массива:

/**
 * 约束参数T为数组类型,
 * 判断T是否为数组,如果是数组类型则推导数组元素的类型
 */
type FalttenArray<T extends Array<any>> = T extends Array<infer P> ? P : never;

/**
 * type Eg1 = number | string;
 */
type Eg1 = FalttenArray<[number, string]>
/**
 * type Eg2 = 1 | 'asd';
 */
type Eg2 = FalttenArray<[1, 'asd']>

ReturnType Получает тип возвращаемого значения функции.

/**
 * @desc ReturnType的实现其实和Parameters的基本一样
 * 无非是使用infer R的位置不一样。
 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

ConstructorParameters

ConstructorParametersВы можете получить типы параметров конструктора класса, хранящиеся в кортеже.

/**
 * 核心实现还是利用infer进行推导构造函数的参数类型
 */
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;


/**
 * @example
 * type Eg = string;
 */
interface ErrorConstructor {
  new(message?: string): Error;
  (message?: string): Error;
  readonly prototype: Error;
}
type Eg = ConstructorParameters<ErrorConstructor>;

/**
 * @example
 * type Eg2 = [name: string, sex?: number];
 */
class People {
  constructor(public name: string, sex?: number) {}
}
type Eg2 = ConstructorParameters<typeof People>
  • Сначала ограничьте параметрыTдля класса с конструктором. Обратите внимание, что естьabstractмодификатор, который будет объяснен позже.
  • Когда осознаешь, судиTэто класс, который удовлетворяет ограничениям, используйтеinfer PАвтоматически выводит тип параметра конструктора и, наконец, возвращает этот тип.

Попади в точку! ! ! Попади в точку! ! ! Попади в точку! ! !

Тогда возникает вопрос, почему T должно быть ограниченоabstractКак насчет абстрактных классов? См. следующий пример:

/**
 * 定义一个普通类
 */
class MyClass {}
/**
 * 定义一个抽象类
 */
abstract class MyAbstractClass {}

// 可以赋值
const c1: typeof MyClass = MyClass
// 报错,无法将抽象构造函数类型分配给非抽象构造函数类型
const c2: typeof MyClass = MyAbstractClass

// 可以赋值
const c3: typeof MyAbstractClass = MyClass
// 可以赋值
const c4: typeof MyAbstractClass = MyAbstractClass

Отсюда видно, что если тип определен как абстрактный класс (абстрактный конструктор), то его можно присвоить абстрактному классу или обычному классу, но не наоборот.

Еще раз в точку! ! ! Еще раз в точку! ! ! Еще раз в точку! ! !

Продолжайте задавать вопросы здесь, используйте классы непосредственно как типы и используйтеtypeof 类Как тип, какая разница?

/**
 * 定义一个类
 */
class People {
  name: number;
  age: number;
  constructor() {}
}

// p1可以正常赋值
const p1: People = new People();
// 等号后面的People报错,类型“typeof People”缺少类型“People”中的以下属性: name, age
const p2: People = People;

// p3报错,类型 "People" 中缺少属性 "prototype",但类型 "typeof People" 中需要该属性
const p3: typeof People = new People();
// p4可以正常赋值
const p4: typeof People = People;

Вывод таков:

  • Когда класс используется непосредственно как тип, ограничение типа заключается в том, что тип должен быть экземпляром класса, то есть тип получает атрибуты экземпляра и методы экземпляра (также называемые методами прототипа) в классе;
  • когда положитьtypeof 类При использовании в качестве типа тип класса, удовлетворяющий ограничениям, то есть тип получает статические свойства и методы класса.

Наконец, лишь быinferВы можете получить тип возвращаемого значения конструктора, изменив позицию:

type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;

Типы, реализованные внутри компилятора Ts

  • Uppercase
/**
 * @desc 构造一个将字符串转大写的类型
 * @example
 * type Eg1 = 'ABCD';
 */
type Eg1 = Uppercase<'abcd'>;
  • Lowercase
/**
 * @desc 构造一个将字符串转小大写的类型
 * @example
 * type Eg2 = 'abcd';
 */
type Eg2 = Lowercase<'ABCD'>;
  • Capitalize
/**
 * @desc 构造一个将字符串首字符转大写的类型
 * @example
 * type Eg3 = 'abcd';
 */
type Eg3 = Capitalize<'Abcd'>;
  • Uncapitalize
/**
 * @desc 构造一个将字符串首字符转小写的类型
 * @example
 * type Eg3 = 'ABCD';
 */
type Eg3 = Uncapitalize<'aBCD'>;

Эти виды инструментов, вlib.es5.d.tsВ файле нет конкретного определения:

type Uppercase<S extends string> = intrinsic;
type Lowercase<S extends string> = intrinsic;
type Capitalize<S extends string> = intrinsic;
type Uncapitalize<S extends string> = intrinsic;

企业微信截图_1900dfc9-3c22-4af2-9523-6860bcf03e03.png

Часть III Пользовательские Ts Advanced Type Tools и навыки программирования типов

SymmetricDifference

SymmetricDifference<T, U>Получает тип, который не существует ни в T, ни в U.

/**
 * 核心实现
 */
type SymmetricDifference<A, B> = SetDifference<A | B, A & B>;

/**
 * SetDifference的实现和Exclude一样
 */
type SymmetricDifference<T, U> = Exclude<T | U, T & U>;

/**
 * @example
 * type Eg = '1' | '4';
 */
type Eg = SymmetricDifference<'1' | '2' | '3', '2' | '3' | '4'>

Его основная реализация использует три пункта: типы распределенного объединения, типы пересечения и исключение.

  • Сначала используйте Exclude, чтобы получить тип, который существует в первом параметре, но не существует во втором параметре.
  • ExcludeВторой параметрT & UПолучает все типы типов пересечения
  • ExcludeПервый параметрT | U, который использует характеристики распределения типа union в extends, что можно понимать какExclude<T, T & U> | Exclude<U, T & U>;

Подводя итог, добыча существует вTно не вT & U, а затем извлеките тип, существующий вUно не вT & U, и, наконец, присоединиться.

FunctionKeys

ПолучатьTво всех типах, которые являются функциямиkeyТип союза, который составляется.

/**
 * @desc NonUndefined判断T是否为undefined
 */
type NonUndefined<T> = T extends undefined ? never : T;

/**
 * @desc 核心实现
 */
type FunctionKeys<T extends object> = {
  [K in keyof T]: NonUndefined<T[K]> extends Function ? K : never;
}[keyof T];

/**
 * @example
 * type Eg = 'key2' | 'key3';
 */
type AType = {
    key1: string,
    key2: () => void,
    key3: Function,
};
type Eg = FunctionKeys<AType>;
  • Во-первых, тип параметра ограничения Tobject
  • по типу отображенияK in keyof TОбход всех ключей, первый проходNonUndefined<T[K]>фильтрT[K]заundefined | nullТип , если он не совпадает, вернуть никогда
  • какT[K]является допустимым типом, затем оцените, является ли онFunctionвведите, если да, вернитеK,в противном случаеnever; тип, который можно получить в это время, например:
/**
 * 上述的Eg在此时应该是如下类型,伪代码:
 */
type TempType = {
    key1: never,
    key2: 'key2',
    key3: 'key3',
}
  • последний через{省略}[keyof T]Доступ к индексу, получение типа объединения типа значенияnever | key2 | key3, после расчетаkey2 | key3;

Попади в точку! ! ! Попади в точку! ! ! Попади в точку! ! !

  • T[]это операция доступа к индексу, тип значения, которое может быть получено
  • T['a' | 'b']как[]Внутренний параметр является типом объединения, который также является характеристикой индекса распределения, а тип значения получается в свою очередь для объединения.
  • T[keyof T]это получитьTтип типа всех значений;
  • neverПри сочетании с другими видамиneverне существует. Например:never | number | stringЭквивалентноnumber | string

Еще раз в точку! ! ! Еще раз в точку! ! ! Еще раз в точку! ! !

  • nullиundefinedЕго можно присвоить другим типам (за исключением строгого определения присваивания в начале типа), поэтому в приведенной выше реализации необходимо использоватьNonUndefinedСначала судите.
  • NonUndefinedреализация в , только оцениваяT extends undefined, на самом деле, потому что они совместимы друг с другом. Таким образом, вы заменитеT extends nullилиT extends null | undefinedВсе возможно.
// A = 1
type A = undefined extends null ? 1 : 2;
// B = 1
type B = null extends undefined ? 1 : 2;

Наконец, если вы хотите написать тип объединения, который получает ключи нефункциональных типов, это не что иное, какKиneverМесто другое. Аналогичным образом вы также можете достичьStringKeys,NumberKeysи Т. Д. Но помните, что вы можете абстрагировать фабричный тип:

type Primitive =
  | string
  | number
  | bigint
  | boolean
  | symbol
  | null
  | undefined;

/**
 * @desc 用于创建获取指定类型工具的类型工厂
 * @param T 待提取的类型
 * @param P 要创建的类型
 * @param IsCheckNon 是否要进行null和undefined检查
 */
type KeysFactory<T, P extends Primitive | Function | object, IsCheckNon extends boolean> = {
  [K in keyof T]: IsCheckNon extends true
    ? (NonUndefined<T[K]> extends P ? K : never)
    : (T[K] extends P ? K : never);
}[keyof T];

/**
 * @example
 * 例如上述KeysFactory就可以通过工厂类型进行创建了
 */
type FunctionKeys<T> = KeysFactory<T, Function, true>;
type StringKeys<T> = KeysFactory<T, string, true>;
type NumberKeys<T> = KeysFactory<T, string, true>;

MutableKeys

MutableKeys<T>найтиTОбъединение всех ключей, не предназначенных только для чтения.

/**
 * 核心实现
 */
type MutableKeys<T extends object> = {
  [P in keyof T]-?: IfEquals<
    { [Q in P]: T[P] },
    { -readonly [Q in P]: T[P] },
    P
  >;
}[keyof T];

/**
 * @desc 一个辅助类型,判断X和Y是否类型相同,
 * @returns 是则返回A,否则返回B
 */
type IfEquals<X, Y, A = X, B = never> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2)
  ? A
  : B;

MutableKeysЕсть еще некоторые трудности, объяснитеMutableKeysРеализация, мы должны разделить следующие шаги:

Первый шаг — понять некоторые особенности режимов только для чтения и не только для чтения.

/**
 * 遍历类型T,原封不动的返回,有点类似于拷贝类型的意思
 */
type RType1<T> = {
  [P in keyof T]: T[P];
}
/**
 * 遍历类型T,将每个key变成非只读
 * 或者理解成去掉只读属性更好理解。
 */
type RType2<T> = {
  -readonly[P in keyof T]: T[P];
}

// R0 = { a: string; readonly b: number }
type R0 = RType1<{a: string, readonly b: number}>

// R1 = { a: string }
type R1 = RType1<{a: string}>;
// R2 = { a: string }
type R2 = RType2<{a: string}>;

// R3 = { readonly a: string }
type R3 = RType1<{readonly a: string}>;
// R4 = { a: string }
type R4 = RType2<{readonly a: string}>;

можно увидеть:RType1иRType2Параметрыне только для чтенияхарактеристики,R1иR2Результат тот же;RType1иRType2Параметрытолько чтениесвойство, полученный результат R3 равентолько чтениеиз,R4дане только для чтенияиз. Итак, вот ключевой момент:

  • [P in Keyof T]Это тип отображения, и отображение является гомоморфным, и гомоморфизм будет копировать исходный модификатор атрибута и т. д. Вы можете обратиться к примеру R0.
  • по типу отображения-readonlyВыражается какне только для чтения, или это можно понимать как удалениетолько чтение. затолько чтениеатрибут плюс-readonlyсталне только для чтения, пока правильноне только для чтенияатрибут плюс-readonlyеще позжене только для чтения. Распространенный метод использования, например, если вы хотите сделать свойства не только для чтения, вы не можете добавлять модификаторы впереди (хотя не запись означает не только для чтения), но вы должны учитывать проблему гомоморфного копирования.

Второй шаг, разбор IfEquals

IfEqualsиспользуется для определения типаXиYЭто то же самое, если они равны, возвратA, иначе возвратB. Эта функция сложнее, не бойтесь, я пойму ее как следует после того, как закончу~

type IfEquals<X, Y, A = X, B = never> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2)
    ? A : B;
  • во-первыхIfEquals<X, Y, A, B>Четыре параметра ,X和Yэто два типа, которые нужно сравнить, и вернуть, если они равныA, возвращает неравноеB.
  • IfEqualsОсновной скелет этоtype IfEquals<> = (参数1) extends (参数2) ? A : BЭто делается для того, чтобы определить, можно ли присвоить тип параметра 1 типу параметра 2, а затем вернутьA, иначе возвратB;
  • Базовая структура параметра 1 и параметра 2 одинакова, разница только в том, что X и Y разные. Вот несколько конкретных примеров:
// A = <T>() => T extends string ? 1 : 2;
type A = <T>() => T extends string ? 1 : 2;
// B = <T>() => T extends number ? 1 : 2;
type B = <T>() => T extends number ? 1 : 2;

// C = 2
type C = A extends B ? 1 : 2;

Не странно ли, почему это можно вывестиAиBТипы разные? сказать вам ответ:

  • Это использует особенность компилятора Ts, то есть компилятор Ts будет думать, что если два типа (например, здесьXиY) используется только для ограничения идентичности двух одинаковых универсальных функций. Это немного непонятно для понимания, или логика не верна (потому что можно привести контрпримеры), но команда разработчиков Ts гарантировала, что эта фича не изменится в будущем. Может относиться кздесь.
  • Обратите внимание, что здесь также оцениваются модификаторы атрибутов, такие какreadonly, 可选属性И так далее, смотрите проверку на следующем примере:
/**
 * T2比T1多了readonly修饰符
 * T3比T1多了可选修饰符
 * 这里控制单一变量进行验证
 */
type T1 = {key1: string};
type T2 = {readonly key1: string};
type T3 = {key1?: string};

// A1 = false
type A1 = IfEquals<T1, T2, true , false>;
// A2 = false
type A2 = IfEquals<T1, T3, true , false>;
  • IfEqualsНаконец, это использование 1 и 2, чтобы помочь суждению (грамматический уровень) и датьAЗначение по умолчаниюX,BЗначение по умолчаниюnever.

Наконец, если ты маленький младенец, который любит (занимается) тренирует (вещи) учится (любит), ты можешь расспросить меня о своей душе: чтобы судить о том, равны ли типы (совместимы), почему бы не использовать их непосредственноtype IfEquals<X, Y, A, B> = X extends Y ? A : BШерстяная ткань? И простой, и грубый (ПС: от твоей злобной очаровательной улыбки~). Ответ, давайте посмотрим на следующий пример:

type IfEquals<X, Y, A, B> = X extends Y ? A : B;

/**
 * 还用上面的例子
 */
type T1 = {key1: string};
type T2 = {readonly key1: string};
type T3 = {key1?: string};

// A1 = true
type A1 = IfEquals<T1, T2, true , false>;
// A2 = true
type A2 = IfEquals<T1, T3, true , false>;

Ответ очевиден, действительно бессильны такие модификаторы, как readonly. Слава когтя Убить~~~

Шаг 3, разборMutableKeysреализовать логику

  • MutableKeysВо-первых, ограничьте T типом object
  • по типу отображения[P in keyof T]Для обхода значение, соответствующее ключу, равноIfEquals<类型1, 类型2, P>, если тип 1 и тип 2 равны, вернуть соответствующий P (то есть ключ), в противном случае вернуть никогда.

иPНа самом деле это тип объединения только с одним текущим ключом, поэтому[Q in P]: T[P]Тоже обычный тип карты. Но обратите внимание, что параметр 1{ [Q in P]: T[P] }через{}Сконструированный тип, параметр 2{ -readonly [Q in P]: T[P] }также через{}Создайте тип, единственная разница между ними заключается в том, что даже если-readonly.

Итак, здесь интересно.Вспомните пример первого шага выше, понимаете ли вы: еслиPдоступен только для чтения, то параметр 1 и параметр 2Pв конечном итоге доступны только для чтения; еслиPне только для чтения, то параметр 1Pдоступен не только для чтения, а параметр 2Pодеяло-readonlyУдален атрибут не только для чтения, чтобы он стал атрибутом только для чтения. Итак, фильтр готов:PКогда не только для чтенияIfEqualsвозвращениеP,Pтолько для чтенияIfEqualsвозвращениеnever.

  • Поэтому, когда ключ не доступен только для чтения, типkey, иначе типnever, наконец через[keyof T]получил все非只读keyтип союза.

OptionalKeys

OptionalKeys<T>Извлеките тип объединения всех необязательных ключей в T.

type OptionalKeys<T> = {
  [P in keyof T]: {} extends Pick<T, P> ? P : never
}[keyof T];

type Eg = OptionalKeys<{key1?: string, key2: number}>
  • Основная реализация проходит через все ключи с типом сопоставления черезPick<T, P>Извлеките текущий ключ и введите. Обратите внимание, что при этом также используется тот факт, что гомоморфная копия копирует необязательные модификаторы.
  • использовать{} extends {当前key: 类型}Проверьте, является ли это необязательным типом.
// Eg2 = false
type Eg2 = {} extends {key1: string} ? true : false;
// Eg3 = true
type Eg3 = {} extends {key1?: string} ? true : false;

использование{}и содержит только необязательные типы параметров{key?: string}совместим с этой функцией. Пучокextendsфронт{}заменитьobjectэто тоже хорошо.

Улучшить выбор

  • PickByValue извлекает тип указанного значения
// 辅助函数,用于获取T中类型不为never的类型组成的联合类型
type TypeKeys<T> = T[keyof T];

/**
 * 核心实现
 */
type PickByValue<T, V> = Pick<T,
  TypeKeys<{[P in keyof T]: T[P] extends V ? P : never}>
>;

/**
 * @example
 *  type Eg = {
 *    key1: number;
 *    key3: number;
 *  }
 */
type Eg = PickByValue<{key1: number, key2: string, key3: number}, number>;

Функция совместимости типов Ts, поэтому что-то вродеstringприсваиваетсяstring | number, поэтому указанный выше метод извлечения не является точным. Если вы реализуете точный способ, вы можете рассмотреть следующие инструменты этого типа.

  • PickByValueExact точно извлекает тип указанного значения
/**
 * 核心实现
 */
type PickByValueExact<T, V> = Pick<T,
  TypeKeys<{[P in keyof T]: [T[P]] extends [V]
    ? ([V] extends [T[P]] ? P : never)
    : never;
  }>
>

// type Eg1 = { b: number };
type Eg1 = PickByValueExact<{a: string, b: number}, number>
// type Eg2 = { b: number; c: number | undefined }
type Eg2 = PickByValueExact<{a: string, b: number, c: number | undefined}, number>

PickByValueExactОсновная реализация имеет три основных момента:

Один из них - использоватьPickизвлекаем то, что нам нужноkeyсоответствующий тип

Во-вторых, использовать слой кортежей, чтобы избежать универсального типа.extendsизТип распределенной федерациихарактеристики

В-третьих, судить о том, являются ли эти два типа одинаковыми или нет, посредством взаимной совместимости.

В частности, см. следующий пример:

type Eq1<X, Y> = X extends Y ? true : false;
type Eq2<X, Y> = [X] extends [Y] ? true : false;
type Eq3<X, Y> = [X] extends [Y]
  ? ([Y] extends [X] ? true : false)
  : false;

// boolean, 期望是false
type Eg1 = Eq1<string | number, string>
// false
type Eg2 = Eq2<string | number, string>

// true,期望是false
type Eg3 = Eq2<string, string | number>
// false
type Eg4 = Eq3<string, string | number>

// true,非strictNullChecks模式下的结果
type Eg5 = Eq3<number | undefined, number>
// false,strictNullChecks模式下的结果
type Eg6 = Eq3<number | undefined, number>
  • отEg1иEg2В сравнении видно, чтоextendsПомещение кортежа в параметр позволяет избежать характеристик распределения, чтобы получить желаемый результат;
  • отEg3иEg4Из сравнения видно, что правильное суждение о равенстве подчиненного типа может быть получено путем оценки того, совместимы ли два типа друг с другом.
  • отEg5иEg6При сравнении видно, чтоstrictNullChecksmode, undefined и null могут быть назначены другим типам свойств, в результате чегоnumber | undefined, numberсовместимы, потому что да и нетstrictNullChecksрежиме, так что этот результат также соответствует ожиданиям. Если вам не нужен этот совместимый результат, вы можете полностью включить егоstrictNullChecksмодель.

В итоге получается тот самый идеалOmitByValueиOmitByValueExactПро основную идею много говорить не буду, каждый может подумать сам.

Intersection

Intersection<T, U>отTизвлечено вUсерединаkeyи соответствующий тип. (обратите внимание, что конец отTизвлечь изkeyи типа)

/**
 * 核心思路利用Pick提取指定的key组成的类型
 */
type Intersection<T extends object, U extends object> = Pick<T,
  Extract<keyof T, keyof U> & Extract<keyof U, keyof T>
>

type Eg = Intersection<{key1: string}, {key1:string, key2: number}>
  • ограничениеTиUобаobject, затем используйтеPickизвлечь указанныйkeyтип композиции
  • пройти черезExtract<keyof T, keyof U>Извлеките ключи, которые существуют как в T, так и в U,Extract<keyof U, keyof T>та же операция

Так зачем2второсортныйExtractА как же кроссоверы? Причина все еще в том, чтобы иметь дело с проблемой совместимого вывода типов, помнитеstringназначаемыйstring | numberСовместим с:

type A = {
    [p: string]: 2
}
type B = {
    aaa: 2
}
// string | number
type AKEY = keyof A;
// "aaa"
type BKEY = keyof B;

// 1
type D = BKEY extends AKEY ? 1 : 2;
// 2
type F = AKEY extends BKEY ? 1 : 2;

Расширение:

определениеDiff<T, U>,отTисключен изUключ и введите .

type Diff<T extends object, U extends object> = Pick<
  T,
  Exclude<keyof T, keyof U>
>;

Перезаписать и назначить

Overwrite<T, U>отUВведите переопределения свойств с тем же именем вTодноименный тип свойства в . (Свойство с тем же именем в последнем переопределяет первое)

/**
 * Overwrite实现
 * 获取前者独有的key和类型,再取两者共有的key和该key在后者中的类型,最后合并。
 */
type Overwrite<
  T extends object,
  U extends object,
  I = Diff<T, U> & Intersection<U, T>
> = Pick<I, keyof I>;

/**
 * @example
 * type Eg1 = { key1: number; }
 */
type Eg1 = Overwrite<{key1: string}, {key1: number, other: boolean}>
  • сдерживать сначалаTиUОба параметраobject
  • При значении параметра I по умолчанию в качестве процесса реализации нет необходимости передавать параметр I при его использовании (только для вспомогательной реализации)
  • пройти черезDiff<T, U>получить возможность существовать вTно не существует вUКлюч и его тип в . (т.е. получитьTуникальный для себяkeyи тип).
  • пройти черезIntersection<U, T>ПолучатьUиTобщийkeyКлюч уже вUвведите . То есть получить последний с тем же именемkeyуже типа.
  • Наконец, перекрестный тип объединяется, так что кривая сохраняет страну для реализации операции покрытия.

Расширения: как реализоватьAssign<T, U>(похожий наObject.assign()) для слияния?

// 实现
type Assign<
  T extends object,
  U extends object,
  I = Diff<T, U> & U
> = Pick<I, keyof I>;

/**
 * @example
 * type Eg = {
 *   name: string;
 *   age: string;
 *   other: string;
 * }
 */
type Eg = Assign<
  { name: string; age: number; },
  { age: string; other: string; }
>;

Подумайте об этом, это просто найти уникальный ключ и тип первого, а затемUПересекать.

DeepRequired

DeepRequired<T>будетTПреобразован в обязательный атрибут. еслиTявляется объектом, рекурсивный объект будет всемkeyпреобразовать вrequired, тип преобразуется вNonUndefined;еслиTЕсли это массив, выполните итерацию по массиву рекурсивно и установите для каждого элемента значениеNonUndefined.

/**
 * DeepRequired实现
 */
type DeepRequired<T> = T extends (...args: any[]) => any
  ? T
  : T extends Array<any>
    ? _DeepRequiredArray<T[number]>
    : T extends object
      ? _DeepRequiredObject<T>
      : T;

// 辅助工具,递归遍历数组将每一项转换成必选
interface _DeepRequiredArray<T> extends Array<DeepRequired<NonUndefined<T>>> {}

// 辅助工具,递归遍历对象将每一项转换成必选
type _DeepRequiredObject<T extends object> = {
  [P in keyof T]-?: DeepRequired<NonUndefined<T[P]>>
}
  • DeepRequiredиспользоватьextendsопределить, является ли это функцией илиPrimitivetype, он возвращает этот тип напрямую.
  • Если это тип массива, используйте_DeepRequiredArrayРекурсивно, и переданный параметр представляет собой тип объединения, состоящий из всех типов подэлементов массива, как показано ниже:
type A = [string, number]
/**
 * @description 对数组进行number索引访问,
 * 得到的是所有子项类型组成的联合类型
 * type B = string | number
 */
type B = A[number]
  • _DeepRequiredArrayЭто интерфейс (он также может быть определен как тип), и его типArray<T>, завершите следующим образом:
Array<
    // DeepRequired的参数最终是个联合类型,会走DeepRequired的子类型分发逻辑进行遍历
    DeepRequired<
        NonUndefined<
            // T[number]实际类似如下:
            T<
                a | b | c | ....
            >
        >
    >
>

и тутTзатем черезDeepRequired<T>делать рекурсию по каждому термину; вTперед использованиемNonUndefined<T>Обработайте один раз и удалите недопустимые типы.

  • Если это объектный тип, с помощью_DeepRequiredObjectРеализовать рекурсивный обход объектов._DeepRequiredObjectПросто обычная переменная типа карты, затем добавьте к каждому ключу-?модификатор преобразуется вrequiredтип.

DeepReadonlyArray

DeepReadonlyArray<T>будетTпреобразуется в режим только для чтения, еслиTзаobjectзатем преобразовать все ключи в режим только для чтения, еслиTПреобразует массив в массив, доступный только для чтения. Весь процесс глубоко рекурсивен.

/**
 * DeepReadonly实现
 */
type DeepReadonly<T> = T extends ((...args: any[]) => any) | Primitive
  ? T
  : T extends _DeepReadonlyArray<infer U>
  ? _DeepReadonlyArray<U>
  : T extends _DeepReadonlyObject<infer V>
  ? _DeepReadonlyObject<V>
  : T;

/**
 * 工具类型,构造一个只读数组
 */
interface _DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

/**
 * 工具类型,构造一个只读对象
 */
type _DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
};
  • основные принципы реализации иDeepRequiredто же самое, но будьте осторожныinfer UАвтоматически определять тип массива,infer VВыводит тип объекта.

UnionToIntersection

Преобразование типов объединения в типы пересечения.

type UnionToIntersection<T> = (T extends any
  ? (arg: T) => void
  : never
) extends (arg: infer U) => void ? U : never
type Eg = UnionToIntersection<{ key1: string } | { key2: number }>
  • T extends any ? (arg: T) => void : neverВыражение должно принимать истинную ветвь и использовать этот метод для построения контравариантного типа объединения.(arg: T1) => void | (arg: T2) => void | (arg: Tn) => void
  • повторно использовать второйextendsСотрудничатьinferПолучите тип U, но используйтеinferправильноСвойства ковариантных типов получают перекрестные типы.

Справочное содержание

При перепечатке просьба указывать автора и источник!