Другая сторона TypeScript: типизированное программирование

внешний интерфейс TypeScript

Добавить Автора

предисловие

Как один из трендов фронтенд-разработки, TypeScript нравится все большему числу разработчиков, на нем написано почти 90% фреймворков и библиотек инструментов (или подобных схем типов, таких как Flow); с небольшой стороны, даже написание файла конфигурации (например, файла конфигурации vite) или небольшого скрипта (спасибо ts-node), TypeScript очень помогает. Невозможно всем что-то нравиться, например, Реми Шарп, автор nodemon, как-то сказал, что никогда не пользовался TS (см.#1565), и не будет изучать TS в будущем, это может быть из-за языковых привычек. Есть еще одна большая гора, которая обычно мешает новичкам начать работу с TypeScript: высокая стоимость обучения.

В начале изучения TypeScript многие студенты его любят и ненавидят.Они неотделимы от его подсказок типов и инженерных возможностей, но их часто беспокоят ошибки типов.В конце концов им приходится пользоваться любыми.Таких случаев много ., TypeScript медленно переписывался в AnyScript...

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

Эта статья предназначена для решения последней проблемы, пытаясь сосредоточиться на части программирования типов в TypeScript (сколько еще частей TS? См. объяснение ниже), начиная с самых основных дженериков и заканчивая индексированием, отображением, условиями и т. д. Введите, затем ключевые слова, такие как is, in, infer, и, наконец, тип инструмента финала. Откройте свою IDE, следуйте за автором, чтобы закончить код раздел за разделом, и поднимите свой уровень TypeScript на новый уровень.

Следует отметить, что эта статья не является вводной статьей по TypeScript и не подходит для студентов, не имеющих опыта работы с TypeScript. Если вы все еще находитесь на стадии новичка, автор рекомендует xcatliuНачало работы с TypeScriptтак же какофициальная документация, по моему личному опыту, вы можете прочитать вводный туториал в начале, а когда запутаетесь, перейти в соответствующий раздел официальной документации.

После завершения базового введения в TypeScript снова добро пожаловать в эту статью.

TypeScript = типизированное программирование + предложение ES

Обычно я делю TypeScript на две части:

  • Предварительно реализованное предложение ES, такие как декораторы, необязательные цепочки?., нулевой оператор объединения??(с необязательным объединением вTypeScript3.7введено в), частные члены классаprivateЖдать. Помимо крайне нестабильного синтаксиса (это вы, декораторы), большинство реализаций TS на самом деле представляют собой будущий синтаксис ES.

    Строго говоря, декоратор текущей версии ES и декоратор версии TS — это две вещи.Подход к MidwayJS: первый взгляд на декораторы TS и механизмы IoCЭта статья знакомит с историей декораторов TS. Заинтересованные студенты могут прочитать их.

    В этой части, независимо от того, есть ли у вас опыт использования языка JavaScript или у вас есть опыт использования Java и C#, вы можете начать очень быстро, ведь это в основном синтаксический сахар. Конечно, это также наиболее часто используемая часть в фактической разработке, в конце концов, и еще одна часть:Тип программированияДля сравнения, эта часть более приземленная.

  • тип программирования, начиная с простогоinterface, чтобы выглядеть достаточно продвинутымT extends SomeType, а затем и к различного рода инструментам, которые не в курсеPartial,RequiredИ т. д., все они подпадают под категорию программирования типов. Эта часть не влияет на фактический функциональный уровень кода, даже если у вас их десять в строке кода, вы столкнетесь с ошибкой типа.@ts-ignore(похожий на@eslint-ignore, отключит проверку типов для следующей строки) или даже включит ее напрямую--transpileOnly(Эта опция отключит возможность проверки типов компилятором TS и будет компилировать только код, что приведет к более высокой скорости компиляции.) Это не повлияет на логику самого вашего кода.
    Однако именно поэтому типизированному программированию не уделялось особого внимания: оно требует много дополнительного кода (код определения типа может даже превышать бизнес-код) по сравнению с синтаксисом. Более того, реальный бизнес не требует таких строгих определений типов.Обычно определяются только данные интерфейса, поток состояния приложения и т. д. Обычно для базовой библиотеки классов фреймворка требуется большой объем кода программирования типов.
    Если предыдущая часть делает код, который вы пишете, более приятным, то самая важная роль этой части — сделать ваш код более элегантным и надежным (да, элегантность и надежность не конфликтуют). Если ваша команда использует платформу мониторинга, такую ​​как Sentry, наиболее распространенными ошибками, совершаемыми с кодом JS, являютсяCannot read property 'xxx' of undefined,undefined is not a functionЭто (смtop-10-javascript-errors), хотя даже ТС не может полностью стереть эту ошибку, он может решить ее девять раз из десяти.

Что ж, после стольких предзнаменований пришло время перейти к сути.Главы этой статьи распределены следующим образом.Если у вас уже есть некоторые базовые знания (например, дженерики), вы можете сразу их пропустить.

  • Основы программирования типов: обобщения

  • Тип охранники и есть, в ключевых словах

  • Типы индексов и типы сопоставления

  • Тип условия, Тип распределенного условия

  • вывести ключевое слово

  • Тип инструмента

  • Новое в TypeScript 4.x

Дженерики

Причина, по которой используются дженерики, заключается в том, что во всей системе программирования типов TypeScript это самая основная часть, и все расширенные типы написаны на ее основе. Точно так же, как мы не можем программировать без переменных, переменные в программировании типов являются дженериками.

Предположим, у нас есть такая функция:

function foo(args: unknown): unknown { ... }
  • Если он получает строку, возвращает частичное усечение этой строки.

  • Если получено число, n раз больше этого количества возвратов.

  • При получении объекта вернуть объект, значение ключа которого было изменено (имя ключа не меняется).

Вышеуказанные сценарии имеют одну общую черту: возвращаемое значение функции имеет тот же тип, что и входной параметр.

Что мне делать, если я хочу получить здесь точное определение типа?

  • Пучокunknownзаменитьstring | number | object? Но это значит, что эта функция принимает любое значение, а возвращаемый тип может быть строкой/числом/объектом, определение типа хоть и есть, но совсем не точное.

Не забывайте, что нам нужноТипы входных и возвращаемых значений одинаковы.Эффект. В настоящее времяДженерикиПора начинать, мы сначала собираем значение типа параметра с помощью дженерика, а затем используем его как возвращаемое значение, вот так:

function foo<T>(arg: T): T {
  return arg;
}

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

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

Кроме того, вы, возможно, виделиArray<number> Map<string, ValueType>Таким образом, мы обычно используем приведенный выше пример вTТакая неназначенная форма становитсяпеременная параметра типаИлиуниверсальный тип, в то время какArray<number>Это было создано под названием实际类型参数илипараметризованный тип.

Обычно дженерики используют только одну букву. Такие как T U K V S и так далее. Моя рекомендуемая практика заключается в использовании общих объявлений переменных с конкретными значениями после того, как проект достигнет определенного уровня сложности, напримерBasicBusinessTypeэта форма.

foo<string>("linbudu");
const [count, setCount] = useState<number>(1);

В приведенном выше примере также можно не указывать, потому что TS автоматически выведет фактический тип универсального типа.В некоторых правилах Lint фактически не рекомендуется добавлять значения типов, которые могут быть выведены автоматически.

Дженерики написаны под стрелочными функциями:

const foo = <T>(arg: T) => arg;

Если вы напишете это в файле TSX,<T>могут быть распознаны как теги JSX, поэтому компилятору необходимо указать явно:

const foo = <T extends SomeBasicType>(arg: T) => arg;

Помимо использования в функциях, дженерики также могут использоваться в классах:

class Foo<T, U> {
  constructor(public arg1: T, public arg2: U) {}

  public method(): T {
    return this.arg1;
  }
}

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

Тип Guard, находится в ключевом слове

Давайте начнем с относительно простого и интуитивно понятного знания: защита типов и обучение от более мелкого к более глубокому программированию типов на основе дженериков.

Допустим есть такое поле, это может быть строка или число:

numOrStrProp: number | string;

Теперь при использовании вы хотите сузить тип объединения этого поля, например, точно доstring, вы можете написать:

export const isString = (arg: unknown): boolean => typeof arg === "string";

Посмотрите на этот эффект:

function useIt(numOrStr: number | string) {
  if (isString(numOrStr)) {
    console.log(numOrStr.length);
  }
}

image

выглядитisStringФункция не сужает область действия типа, и параметр по-прежнему является типом объединения. Это время следует использоватьisКлючевое слово тоже:

export const isString = (arg: unknown): arg is string =>
  typeof arg === "string";

В это время, если вы воспользуетесь им снова, вы обнаружите, что вisString(numOrStr)дляtrueназад,numOrStrТип сужается доstring. Это просто тип объединения с примитивными типами в качестве членов. Мы можем расширить его до различных сценариев. Давайте сначала рассмотрим простое ложное оценочное суждение:

export type Falsy = false | "" | 0 | null | undefined;

export const isFalsy = (val: unknown): val is Falsy => !val;

Это должен быть один из самых типичных псевдонимов, которые я использую каждый день, похожий иisPrimitive,isFunctionОхранники такого типа.

А используя ключевое слово in, мы можем еще больше сузить тип (Type Narrowing), рассмотрим следующий пример, как уменьшить тип объединения "A | B" до "A"?

class A {
  public a() {}

  public useA() {
    return "A";
  }
}

class B {
  public b() {}

  public useB() {
    return "B";
  }
}

Сначала подумайте оfor...inCycle, он обходит имя объекта объекта, иinТо же самое верно и для ключевых слов, которые могут определить, принадлежит ли свойство объекту:

function useIt(arg: A | B): void {
  'a' in arg ? arg.useA() : arg.useB();
}

если параметр существуетaатрибут, так как пересечение двух типов А и В не содержит а, это может немедленно сузить суждение о типе до А.

Поскольку пересечение двух типов А и В не содержит атрибута а, тоinПредикат сужает соответствие типов точно до начала и конца троичного выражения. то есть А или Б.

Вот еще один пример использования литерала в качестве защиты типа:

interface IBoy {
  name: "mike";
  gf: string;
}

interface IGirl {
  name: "sofia";
  bf: string;
}

function getLover(child: IBoy | IGirl): string {
  if (child.name === "mike") {
    return child.gf;
  } else {
    return child.bf;
  }
}

О литеральных типахliteral types, это еще одно ограничение на тип, например, ваш код состояния может быть только 0/1/2, тогда вы можете написатьstatus: 0 | 1 | 2форме вместо использованияnumberвыражать.

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

  • Строковые литералы, такие какmode: "dev" | "prod".

  • Булевы литералы часто смешиваются с литералами других типов, такими какopen: true | "none" | "chrome".

Такого рода детальное базовое знание будет объясняться в различных частях текста, чтобы избежать отсутствия конкретных сцен при объяснении его одного и сделать связанные концепции слишком монотонными.

Различать интерфейсы на основе полей

Я часто вижу студентов, задающих подобные вопросы в повседневной жизни: информация о пользователе под логином и не вошедшим в систему — это совершенно другой интерфейс, или это

Младший брат задавал вопрос ранее.Я думаю, многие мелкие партнеры, которые используют TS для написания интерфейсов, могли столкнуться с тем, что, то есть информация о пользователе под логином и нелогином - это совершенно разные интерфейсы (или аналогичные, которые должны быть основаны на атрибутах, поле для различения разных интерфейсов), на самом деле вы также можете использоватьinРешение ключевого слова:

interface ILogInUserProps {
  isLogin: boolean;
  name: string;
}

interface IUnLoginUserProps {
  isLogin: boolean;
  from: string;
}

type UserProps = ILogInUserProps | IUnLoginUserProps;

function getUserInfo(user: ILogInUserProps | IUnLoginUserProps): string {
  return 'name' in user ? user.name : user.from;
}

или через литеральный тип:

interface ICommonUserProps {
  type: "common",
  accountLevel: string
}

interface IVIPUserProps {
  type: "vip";
  vipLevel: string;
}

type UserProps = ICommonUserProps | IVIPUserProps;

function getUserInfo(user: ICommonUserProps | IVIPUserProps): string {
  return user.type === "common" ? user.accountLevel : user.vipLevel;
}

Таким же образом вы также можете использоватьinstanceofЧтобы выполнить защиту типа экземпляра, рекомендуется проявить смекалку и попробовать.

Типы индексов и типы сопоставления

тип индекса

Перед прочтением этой части нужно быть готовым к сдвигу мышления, нужно реально осознатьТиповое программирование на самом деле программирование, потому что отсюда мы действительно будем выполнять всевозможные причудливые операции с дженериками в качестве переменных.

Точно так же, когда вы пишете бизнес-код, вы часто проходите через объект, а в программировании типов мы часто проходим через интерфейс. Поэтому вы можете полностью переиспользовать некоторые программные идеи. Сначала реализуйте простую функцию, которая возвращает определенный ключ объекта:

// 假设key是obj键名
function pickSingleValue(obj, key) {
  return obj[key];
}

Что нужно определить для определения типа?

  • параметрobj

  • параметрkey

  • возвращаемое значение

Между этими тремя есть определенная связь:

  • keyдолжно бытьobjОдно из имен ключ-значение в , и должно бытьstringТип (обычно мы просто использовали строки в качестве ключей объекта)

  • Возвращенное значение должно бытьключевое значение в объекте

Итак, изначально мы получаем следующие результаты:

function pickSingleValue<T>(obj: T, key: keyof T) {
  return obj[key];
}

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

interface foo {
  a: number;
  b: string;
}

type A = keyof foo; // "a" | "b"

Это какObject.keys()Такой же? Разница в том, что он возвращает тип объединения.

тип союзаUnion Typeобычно используется|Синтаксис, представляющий несколько возможных значений, фактически использовался в самом начале. Основным вариантом использования типов объединения является раздел условного типа, который будет объяснен в отдельной главе позже.

Возвращаемое значение также меньше.Если вы раньше не сталкивались с таким синтаксисом, вы должны застрять.Давайте сначала подумаем об этом.for...inСинтаксис, при обходе объектов мы могли бы написать:

const fooObj = { a: 1, b: "1" };

for (const key in fooObj) {
  console.log(key);
  console.log(fooObj[key]);
}

Как и выше, мы можем получить соответствующее значение при получении ключа, поэтому тип значения проще:

function pickSingleValue<T>(obj: T, key: keyof T): T[keyof T] {
  return obj[key];
}

Эту часть может быть нелегко понять за один шаг, объясните:

interface T {
 a: number;
 b: string;
}

type TKeys = keyof T; // "a" | "b"

type PropAType = T["a"]; // number

Вы можете использовать имя ключа для извлечения значения ключа из объекта, и, естественно, вы можете извлечь значение ключа (то есть тип) из интерфейса~

Но этот способ написания явно имеет место для улучшения:keyofпоявляется дважды, и универсальный T должен фактически быть ограничен объектными типами. Во-первых, как мы обычно делаем в программировании: используйте переменную для хранения нескольких вхождений, помните,В программировании типов дженерики — это переменные..

function pickSingleValue<T extends object, U extends keyof T>(
  obj: T,
  key: U
): T[U] {
  return obj[key];
}

Вот что-то новоеextends... что это такое? вы можете временноT extends objectпонимается какT ограничен типами объектов,U extends keyof TПодразумевается, что универсальный U должен быть типом объединения, состоящим из имен ключей универсального T (в виде литерального типа, например, имя ключа объекта T включает abc, тогда значение U может только быть "а" "б" "один из с", т.е."a" | "b" | "c"). Подробности будут рассмотрены в главе «Типы условий».

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

function pick<T extends object, U extends keyof T>(obj: T, keys: U[]): T[U][] {
  return keys.map((key) => obj[key]);
}

// pick(obj, ['a', 'b'])

Есть два важных изменения:

  • keys: U[]Мы знаем, что U является типом объединения, состоящим из имен ключей T, поэтому этот метод можно использовать для представления массива, внутренние элементы которого являются именами ключей T. Конкретный принцип см. в следующем разделе.Тип распределенного условияглава.

  • T[U][]Его принцип на самом деле такой же, как и выше, в первую очередьT[U], который представляет ключевое значение параметра 1 (например,Object[Key]), я думаю, что это хороший пример композиционной природы программирования типа TS, вам не кажется, что такой способ написания подобен строительным блокам?

Подпись индекса Подпись индекса

В JavaScript мы обычно используемarr[1]способ индексации массива, используйтеobj[key]способ индексации объектов. Проще говоря, индекс — это то, как вы получаете члены объекта, а в программировании типов сигнатура индекса используется для быстрого построения интерфейса с тем же внутренним типом поля, например

interface Foo {
  [keys: string]: string;
}

Тогда интерфейс Foo фактически эквивалентен интерфейсу, все ключи и значения которого имеют строковый тип и не ограничивают членов.

ЭквивалентноRecord<string, string>, см. Типы инструментов.

Стоит отметить, что, поскольку JS может обращаться к свойствам объекта как через числа, так и через строки, поэтомуkeyof Fooрезультат будетstring | number.

const o: Foo = {
 1: "芜湖!",
};

o[1] === o["1"]; // true

Но как только индексный тип подписи интерфейсаnumber, то объект, использующий его, больше не может быть доступен по строковому индексу, напримерo['1'], выдаст ошибку,Элемент неявно имеет тип «любой», поскольку выражение индекса не имеет типа «число».

Сопоставленные типы

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

  • Изменить тип значения ключа исходного интерфейса

  • Добавьте модификаторы к исходному типу ключа интерфейса, напримерreadonlyс дополнительным?

Начните с простого сценария:

interface A {
  a: boolean;
  b: string;
  c: number;
  d: () => void;
}

Теперь у нас есть требование реализовать интерфейс, поля которого точно такие же, как у интерфейса A, но все типы которогоstring,Что бы ты сделал? Просто повторно объявить один и написать его вручную? Это возмутительно, а мы находчивые программисты.

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

type StringifyA<T> = {
  [K in keyof T]: string;
};

Это знакомо? Важно то, что этоinоператора, вы можете понять это какfor...in/for...ofПри таком обходе после получения имени ключа значение ключа оказывается простым, поэтому мы можем легко скопировать псевдоним нового типа.

type ClonedA<T> = {
  [K in keyof T]: T[K];
};

Освоив эту идею, вы, по сути, уже соприкоснулись с базовой реализацией некоторых типов инструментов:

Вы можете думать о типе инструмента какПубличные функции, которые вы обычно помещаете в папку utils, обеспечивают инкапсуляцию общедоступной логики (здесь введите программную логику)., например, интерфейсы двух типов выше. Дополнительные типы инструментов см. в главе «Типы инструментов».

Сначала напишите самые распространенныеPartialПопробуйте пораньше, мы расширим подробное знакомство с типами инструментов в специальных главах:

// 将接口下的字段全部变为可选的
type Partial<T> = {
  [K in keyof T]?: T[k];
};

key?: valueозначает, что это поле является необязательным и в большинстве случаев эквивалентноkey: value | undefined.

Условные типы

Когда мы сталкиваемся с условными суждениями в программировании, мы часто используем операторы If и тернарные выражения для их реализации Лично я предпочитаю последнее, даже если оно:

if (condition) {
  execute()
}

Этот оператор If без else я тоже привык писать:

condition ? execute() : void 0;

Синтаксис условных типов на самом деле представляет собой тернарное выражение, рассмотрим простейший пример:

T extends U ? X : Y

Если вы думаете, что здесь не так просто понять расширение, вы можете временно понять, что все свойства в U находятся в T.

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

По аналогии с операторами программирования, это фактически динамическое присвоение значений переменных в соответствии с условными суждениями:

let unknownVar: string;

unknownVar = condition ? "淘系前端" : "淘宝FED";

type LiteralType<T> = T extends string ? "foo" : "bar";

Тип условия на самом деле очень интуитивно понятен.Единственное, что должно иметь определенную стоимость понимания, этоКогда система условного типа соберет достаточно информации для определения типа, то есть тип условия иногда не завершает оценку немедленно.Например, функция, предоставляемая библиотекой инструментов, требует, чтобы пользователь передал параметры при ее использовании для завершения оценки типа условия.

Прежде чем понять это, давайте рассмотрим распространенный сценарий для условных типов:общие ограничения, который на самом деле является нашим примером типа индекса выше:

function pickSingleValue<T extends object, U extends keyof T>(
  obj: T,
  key: U
): T[U] {
  return obj[key];
}

здесьT extends objectа такжеU extends keyof Tявляются общими ограничениями, соответственноограничить T как тип объектаа такжеОграничить U, чтобы он был буквальным типом объединения имен ключей T(Не помню? Подсказка:1 | 2 | 3). Обычно мы используем общие ограниченияОграничения узкого типаКороче говоря, сами дженерики открыты для всех, и все типы могут бытьЯвно передается в(Такие какArray<number>) илиНеявный вывод(Такие какfoo(1)) Это на самом деле не то, что мы хотим, так как мы иногда проверяем параметры функции:

function checkArgFirst(arg){
  if(typeof arg !== "number"){
    throw new Error("arg must be number type!")
  }
}

В TS через универсальные ограничения мы требуем, чтобы входящий дженерик мог быть только фиксированного типа, напримерT extends {}ограничивать дженерики типами объектов,T extends number | stringОграничьте дженерики числовыми и строковыми типами.

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

declare function strOrNum<T extends boolean>(
  x: T
): T extends true ? string : number;

В этом случае вывод условного типа задерживается, потому что в системе типов недостаточно информации для завершения решения.

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

const strReturnType = strOrNum(true);
const numReturnType = strOrNum(false);

Точно так же, как тернарные выражения могут быть вложенными, условные типы также могут быть вложенными.Если вы посмотрите на исходный код какой-либо платформы, вы также обнаружите, что в нем много вложенных условных типов.Никакие другие условные типы не могут ограничивать типы. в очень узкий диапазон, обеспечивая точные типы условий, такие как:

type TypeName<T> = T extends string
  ? "string"
  : T extends number
  ? "number"
  : T extends boolean
  ? "boolean"
  : T extends undefined
  ? "undefined"
  : T extends Function
  ? "function"
  : "object";

Распределенные условные типы

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

оригинал:

Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation

Сначала извлеките несколько ключевых слов, а затем поясним концепцию на примерах:

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

  • создавать экземпляр

  • Распределить по типу союза

    // используем псевдоним типа TypeName выше

    // "string" | "function" type T1 = TypeName<string | (() => void)>;

    // "string" | "object" type T2 = TypeName<string | string[]>;

    // "object" type T3 = TypeName<string[] | number[]>;

Мы обнаружили, что в приведенном выше примере результаты вывода условных типов являются всеми типами объединения (T3 на самом деле один и тот же, но он объединен, потому что результаты одинаковы), и на самом деле параметры типа условно оцениваются в последовательности , а затем использовал|результат комбинации.

Получил что-то? В приведенном выше примере дженерики все выставляются. Если они обернуты, будут изменять результат суждения условного типа? Давайте посмотрим на другой пример:

type Naked<T> = T extends boolean ? "Y" : "N";
type Wrapped<T> = [T] extends [boolean] ? "Y" : "N";

// "N" | "Y"
type Distributed = Naked<number | boolean>;

// "N"
type NotDistributed = Wrapped<number | boolean>;
  • Среди них псевдоним распределенного типа, параметр его типа (number | boolean) будет распределяться правильно, т.е.
    распространятьNaked<number> | Naked<boolean>, а потом судить, так что результат"N" | "Y".

  • Что касается алиаса типа NotDistributed, то на первый взгляд кажется, что TS должен автоматически распределяться как массив, и результат должен быть таким же"N" | "Y"? А на самом деле это тип параметров (number | boolean) Не имеют процесса раздачи, напрямую[number | boolean] extends [boolean], так что результат"N".

Теперь мы можем поговорить об этих понятиях:

  • Параметры голого типа, никаких дополнений нет.[]Wrapped, как если бы он был обернут массивом, больше нельзя назвать параметром голого типа.

  • На самом деле создание экземпляра — это процесс оценки типа условия.Как мы уже говорили ранее, тип условия должен собрать достаточно информации для вывода, чтобы выполнить этот процесс. Процесс создания двух примеров здесь на самом деле отличается, что будет описано в следующем пункте.

  • Распределите к суставному типу:

  • Для TypeName его внутренний параметр типа T не заключен в оболочку, поэтомуTypeName<string | (() => void)>будет распространяться какTypeName<string> | TypeName<(() => void)>, а затем снова судить, и, наконец, распределить как"string" | "function".

  • Конкретный процесс в абстракции:

    ( A | B | C ) extends T ? X : Y
    // 相当于
    (A extends T ? X : Y) | (B extends T ? X : Y) | (B extends T ? X : Y)
    
    // 使用[]包裹后,不会进行额外的分发逻辑。
    [A | B | C] extends [T] ? X : Y
    

    Вкратце в одном предложении:не был[]Дополнительно упакованный параметр типа объединения будет распространять тип объединения, когда оценивается тип условия, и судить отдельно.

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

вывести ключевое слово

В условном типе мы показываем, как задержать определенный тип с помощью условного суждения, но использовать только тип условия: он не может получить информацию о типе из условия. Например,T extends Array<PrimitiveType> ? "foo" : "bar"В этом примере мы не можем вывести из условногоArray<PrimitiveType>получено вPrimitiveTypeфактический тип.

И такие сценарии очень распространены, например, получение типа возвращаемого значения функции, распаковка промиса/массива и т. д., поэтому в этом разделе будут представлены следующиеinferключевые слова.

inferдаinferenceАббревиатура, обычно используемая для изменения обобщений в качестве параметров типа, например:infer R,RвыражатьТип для вывода. как правилоinferНе используется напрямую, а помещается в базовый тип инструмента вместе с условным типом. Если условные типы предоставляют возможность отложенного вывода, добавьтеinferОн предоставляет возможность выполнять отложенный вывод на основе условий.

См. простой пример типа утилиты для получения типа возвращаемого значения функции.ReturnType:

const foo = (): string => {
  return "linbudu";
};

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// string
type FooReturnType = ReturnType<typeof foo>;
  • (...args: any[]) => infer Rпредставляет собой целое, где позиция типа возвращаемого значения функцииinfer Rзаняты.

  • когдаReturnTypeвызывается, и параметры типа T и R назначаются явно (Ttypeof foo,infer Rназначается в целомstring, то есть тип возвращаемого значения функции), если T удовлетворяет ограничениям условного типа, возвращается значение R после вывода, где R — фактический тип возвращаемого значения функции.

  • На самом деле, для строгости общий тип T должен быть ограничен функциональным типом, т.е.:

    // 第一个 extends 约束可传入的泛型只能为函数类型
    // 第二个 extends 作为条件判断
    type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never;
    

inferИдея использования .Loading/ Скелетный экран, визуализируйте реальные данные после возврата запроса.inferЭто та же идея,После того, как система типов получит достаточно информации (обычно из ленивого вывода условий), она может вывести параметры типа, которые следуют за выводом., который обычно возвращает этот результат вывода в конце.

Точно так же с помощью этой идеи мы также можем получить типы входных параметров функции, типы входных параметров конструктора класса и даже внутренние типы Promise и т. д. Мы поговорим об этих типах инструментов позже.

Кроме того, в случае перегрузки функций в TS используйте infer (как указано вышеReturnType) не будет выполнять процесс вывода для всех перегрузок, будет использоваться только последняя перегрузка (поскольку обычно последняя перегрузка обычно является самым широким случаем).

Тип инструмента Тип инструмента

Эта глава должна быть самой «рентабельной» частью этой статьи, потому что даже после прочтения этой части вы все еще не совсем понимаете, как реализованы эти типы инструментов, и это не влияет на то, как правильно вы их используете, просто как Lodash не Это потребует от вас знакомства с принципами каждой функции, которую вы используете.

Эта часть включаетТипы встроенных инструментов TSс сообществомТип инструмента расширения, я лично рекомендую выбрать некоторые типы инструментов для записи после завершения исследования, например, вы считаете, что это более ценно, может использоваться существующим или будущим бизнесом, или просто тип инструментов, который вы считаете очень интересным, и создать новый в вашем собственном проекте.d.tsфайл (или/utils/tool-types.tsвот так) для его хранения.

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

Типы встроенных инструментов

Выше мы реализовали наиболее часто используемые типы встроенных инструментов:

type Partial<T> = {
  [K in keyof T]?: T[k];
};

Он используется, чтобы сделать все поля в интерфейсе необязательными, за исключением типов индексов и типов сопоставления, он использует только?необязательный модификатор, то я сейчас просто вытащу шпаргалку:

  • Удалите необязательные модификаторы:-?, место с?последовательный

  • Модификатор только для чтения:readonly, позиция указана в имени ключа, напримерreadonly key: string

  • Удалите модификатор только для чтения:-readonly, то же местоreadonly.

поздравляю, ты понялRequiredа такжеReadonly(УдалитьreadonlyТип инструмента модификатора не является встроенным, как мы увидим позже):

type Required<T> = {
  [K in keyof T]-?: T[K];
};

type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

Выше мы реализовали функцию выбора:

function pick<T extends object, U extends keyof T>(obj: T, keys: U[]): T[U][] {
  return keys.map((key) => obj[key]);
}

Точно так же предположим, что теперь нам нужно выбрать некоторые поля из интерфейса:

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

// 期望用法
// 期望结果 A["a"]类型 | A["b"]类型
type Part = Pick<A, "a" | "b">;

Это по-прежнему тип сопоставления, но теперь источник сопоставления типа сопоставления передается вPickТиповой параметр К.

Так как естьPick, то естественноOmit(Один состоит в том, чтобы выбрать части от объекта, один - исключить части), это иPickпишется очень похоже, но есть проблема, которую нужно решить: как мы представимTисключен изKпосле остальных полей?

Pick выбирает входящий ключ, а Omit удаляет входящий ключ.

Здесь мы должны ввести еще одну точку знаний:neverтип, который представляет тип, который никогда не встречается и часто используется для преобразованияСузить тип объединения или интерфейс, или как итог для условного суждения о типе. Вы можете увидеть в деталяхответ Юды, здесь мы не расширяем введение.

Приведенный выше сценарий можно упростить до:

// "3" | "4" | "5"
type LeftFields = Exclude<"1" | "2" | "3" | "4" | "5", "1" | "2">;

Exclude, буквально означает исключение, тогда первый параметр должен быть фильтруемым, а второй должен быть условием фильтрации! Попробуйте сначала так:

По сути, здесь используются характеристики распределенных условных типов.Предполагая, что Exclude получает два параметра типа TU, тип в типе объединения T будет оцениваться по типу U. Если параметр типа находится в U, он будет устранен (присвоение никогда).

Заземленная версия:"1"существует"1" | "2"внутри? ("1" extends "1"|"2" -> true)? Если он существует, удалите его (назначьте егоnever), сохраните его, если его там нет.

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

Тогда Omit очень прост: для членов исходного интерфейса удалите входящие члены типа union и примените Pick.

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

спойлер,Почти во всех сценариях, использующих условные типы, если вы отмените оператор присваивания после решения, возникнут новые сценарии.,НапримерExcludeУдалите имя ключа, затем наоборот сохраните имя ключа:

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

Давайте посмотрим на распространенный тип инструментаRecord<Keys, Type>, обычно используемый для генерации объединенных типов с ключом (Keys), тип ключевого значенияTypeНовые интерфейсы, такие как:

type MyNav = "a" | "b" | "b";
interface INavWidgets {
  widgets: string[];
  title?: string;
  keepAlive?: boolean;
}
const router: Record<MyNav, INavWidgets> = {
  a: { widget: [""] },
  b: { widget: [""] },
  c: { widget: [""] },
};

На самом деле все просто,KeysВыньте каждое ключевое значение , а тип указан какTypeТолько что

// K extends keyof any 约束K必须为联合类型
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

Обратите внимание, что запись также поддерживаетRecord<string, unknown>В этом случае,string extends keyof anyустанавливается также потому, чтоkeyofКонечный результат должен бытьstringТип объединения, которое формируется (за исключением случаев, когда числа используются в качестве имен ключей...).

В предыдущем разделе вывода мы реализовали метод, используемый для получения возвращаемого значения функции.ReturnType:

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

По сути, измените положение infer, например, поставьте его во входной параметр, он станет тот, который получает тип параметра.Parameters:

type Parameters<T extends (...args: any) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : never;

Если вы наберетесь смелости и замените обычную функцию конструктором класса, то получите тип параметра конструктора класса.ConstructorParameters:

type ConstructorParameters<
  T extends new (...args: any) => any
> = T extends new (...args: infer P) => any ? P : never;

плюсnewКлючевые слова, чтобы сделать этообъявление инстанцируемого типа, то есть общее ограничение здесьДобрый.

Это для получения типа входного параметра конструктора класса.Если вы помещаете тип, который будет выводиться при его возврате, подумайте о возвращаемом значении нового класса? Пример! Итак, мы получаем тип экземпляраInstanceType:

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

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

Тип инструмента сообщества

Большинство типов инструментов в этом разделе взяты изutility-types, авторы которого такжеreact-redux-typescript-guideа такжеtypesafe-actionsЭти две прекрасные работы.

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

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

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

export const isPrimitive = (val: unknown): val is Primitive => {
  if (val === null || val === undefined) {
    return true;
  }

  const typeDef = typeof val;

  const primitiveNonNullishTypes = [
    "string",
    "number",
    "bigint",
    "boolean",
    "symbol",
  ];

  return primitiveNonNullishTypes.indexOf(typeDef) !== -1;
};

export type Nullish = null | undefined;

export type NonUndefined<A> = A extends undefined ? never : A;
// 实际上TS也内置了这个工具类型
type NonNullable<T> = T extends null | undefined ? never : T;

Falsyа такжеisFalsyМы показали выше.

Воспользовавшись памятью infer, давайте рассмотрим распространенный сценарий извлечения фактического типа промиса:

const foo = (): Promise<string> => {
  return new Promise((resolve, reject) => {
    resolve("linbudu");
  });
};

// Promise<string>
type FooReturnType = ReturnType<typeof foo>;

// string
type NakedFooReturnType = PromiseType<FooReturnType>;

Если вы уже владеетеinferиспользуется, то это на самом деле хорошо написано, просто используйтеinferПараметр можно использовать как общий тип Promise:

export type PromiseType<T extends Promise<any>> = T extends Promise<infer U>
  ? U
  : never;

использоватьinfer Rждать, пока система типов выведетRконкретный тип.

рекурсивный тип инструмента

Ранее мы писалиPartial Readonly RequiredЕсть несколько типов инструментов, модифицирующих поля интерфейса, но на самом деле они имеют ограничения: что делать, если в интерфейсе есть вложенность?

type Partial<T> = {
  [P in keyof T]?: T[P];
};

Логика:

  • Если это не тип объекта, просто добавьте?модификатор

  • Если это объектный тип, топеребирать объект

  • Повторите описанный выше процесс.

Мы видели суждение о том, является ли это типом объекта много раз,T extends objectВот и все, так как же перемещаться внутри объекта? Это на самом деле рекурсивно.

export type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

utility-typesВнутренняя реализация на самом деле сложнее, и ситуация с массивом также рассматривается, здесь она упрощена для простоты понимания, и последние типы инструментов также имеют такие упрощения.

ТакDeepReadobly,DeepRequiredЭто так же просто, как:

export type DeepMutable<T> = {
  -readonly [P in keyof T]: T[P] extends object ? DeepMutable<T[P]> : T[P];
};

// 即DeepReadonly
export type DeepImmutable<T> = {
  +readonly [P in keyof T]: T[P] extends object ? DeepImmutable<T[P]> : T[P];
};

export type DeepRequired<T> = {
  [P in keyof T]-?: T[P] extends object | undefined ? DeepRequired<T[P]> : T[P];
};

Обратите особое вниманиеDeepRequired, о его состоянии судят поT[P] extends object | undefined, потому что вложенные типы объектов могут быть необязательными (неопределенными), что может привести к неправильным результатам, если используется только объект.

Другой беззаботный способ - не судить о типе условия и напрямую рекурсивно использовать все атрибуты~

Тип инструмента, который возвращает имя ключа

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

  • необязательные/обязательные/только для чтения/не для чтения поля

  • Поля (не)объектов/(не)функций/типов

Давайте взглянем на простейшее поле типа функцииFunctionTypeKeys:

export type FunctTypeKeys<T extends object> = {
  [K in keyof T]-?: T[K] extends Function ? K : never;
}[keyof T];

{[K in keyof T]: ... }[keyof T]Такой способ написания может показаться немного странным, давайте разберем его по частям:

interface IWithFuncKeys {
  a: string;
  b: number;
  c: boolean;
  d: () => void;
}

type WTFIsThis<T extends object> = {
  [K in keyof T]-?: T[K] extends Function ? K : never;
};

type UseIt1 = WTFIsThis<IWithFuncKeys>;

легко вывестиUseIt1На самом деле это:

type UseIt1 = {
  a: never;
  b: never;
  c: never;
  d: "d";
};

UseItВсе поля будут сохранены.Значением ключа поля, удовлетворяющего условию, является литеральный тип (т. е. имя ключа), а полем, не удовлетворяющим условию, - никогда.

Добавьте следующую часть:

// "d"
type UseIt2 = UseIt1[keyof UseIt1];

Процесс аналогичен перестановкам и комбинациям:neverзначение типа не отображается в типе объединения

// never类型会被自动去除掉 string | number
type WithNever = string | never | number;

так{ [K in keyof T]: ... }[keyof T]Этот способ написания фактически возвращает имя ключа (подготовлено сказать, что этосоюзный тип имен ключей).

Тогда поля нефункционального типа тоже очень простые, поэтому я не буду их здесь показывать Давайте посмотрим на необязательные поля.OptionalKeysс обязательными полямиRequiredKeys, давайте рассмотрим небольшой пример:

type WTFAMI1 = {} extends { prop: number } ? "Y" : "N";
type WTFAMI2 = {} extends { prop?: number } ? "Y" : "N";

Если вы можете обойти это, легко получить ответ. Если не обходить его какое-то время, то очень просто.Для предыдущего случая,propтребуется, поэтому пустой объект{}не удовлетворяетextends { prop: number }, и да для реквизита, который является необязательным.

Итак, мы используем эту идею для получения необязательных/обязательных имен ключей.

  • {} extends Pick<T, K>,еслиKЕсли это необязательное поле, оставьте его (OptionalKeys, если это RequiredKeys, удалите его).

  • Как избавиться от этого? Конечно использоватьnever.

    export type RequiredKeys = { [K in keyof T]-?: {} extends Pick<T, K> ? never : K; }[keyof T];

Вот удаление необязательных полей, затем сохраняются необязательные ключи:

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

поле только для чтенияIMmutableKeysс полями не только для чтенияMutableKeysИдея аналогична, то есть сначала получаем:

interface MutableKeys {
  readonlyKeys: never;
  notReadonlyKeys: "notReadonlyKeys";
}

тогда не получайneverимя поля.

Я все же хочу выразить свое восхищение автором здесь.Это действительно гениально.Во-первых, определить тип инструмента.IfEqual, сравните, являются ли эти два типа одинаковыми, и даже сравните случай до и после модификации, то есть случай только для чтения и не только для чтения здесь.

type Equal<X, Y, A = X, B = never> = (<T>() => T extends X ? 1 : 2) extends <
  T
>() => T extends Y ? 1 : 2
  ? A
  : B;
  • не будь<T>() => T extends X ? 1 : 2Интерференцию можно понимать как оболочку для сравнения, которая может различать свойства только для чтения и не только для чтения. который(<T>() => T extends X ? 1 : 2)эту часть, только если параметр типаXКогда точно так же, два(<T>() => T extends X ? 1 : 2)` будет конгруэнтным, и эта согласованность требует, чтобы модификации, такие как только для чтения и необязательные, также были согласованы.

  • При фактическом использовании (в качестве примера возьмем случай не только для чтения) мы передаем интерфейс для X и удаляем атрибут только для чтения для прохода Y в-readonlyИнтерфейс, в котором все ключи один раз сравниваются с ключами с удаленными атрибутами только для чтения. Передайте имя поля для A, а B здесь никогда не бывает, поэтому его можно оставить пустым.

Пример:

export type MutableKeys<T extends object> = {
  [P in keyof T]-?: Equal<
    { [Q in P]: T[P] },
    { -readonly [Q in P]: T[P] },
    P,
    never
  >;
}[keyof T];

Несколько моментов, которые легко обойти:

  • Общий Q здесь фактически не используется, просто заполнитель поля для отображаемого типа.

  • Х, у есть такжеТип распределенного условия, чтобы сравнить поля до и после удаления readonly.

Так же есть:

export type IMmutableKeys<T extends object> = {
  [P in keyof T]-?: Equal<
    { [Q in P]: T[P] },
    { -readonly [Q in P]: T[P] },
    never,
    P
  >;
}[keyof T];
  • не здесьreadonlyОперация модификатора, а оператор суждения, который меняет тип условия.

Выбор и пропуск на основе типов значений

Перед нами, чтобы достичь Pick and Omit, он основан на именах ключей, предполагая, что теперь нам нужно сделать тип в соответствии с выбранным значением, отклонить его?

На самом деле все очень просто, этоT[K] extends ValueTypeТолько что:

export type PickByValueType<T, ValueType> = Pick<
  T,
  { [Key in keyof T]-?: T[Key] extends ValueType ? Key : never }[keyof T]
>;

export type OmitByValueType<T, ValueType> = Pick<
  T,
  { [Key in keyof T]-?: T[Key] extends ValueType ? never : Key }[keyof T]
>;

Типы условий берут на себя слишком много...

Список типов инструментов

Подводя итог типам инструментов, которые мы написали выше:

  • Полный интерфейс модификации:Partial Readonly(Immutable) Mutable Requiredи соответствующую рекурсивную версию.

  • Интерфейс отсечения:Pick Omit PickByValueType OmitByValueType

  • На основании выводов:ReturnType ParamType PromiseType

  • Получить указанное поле условия:FunctionKeys OptionalKeys RequiredKeys ...

Следует отметить, что иногда один тип инструмента не соответствует вашим требованиям, вам может потребоваться несколько типов инструментов для совместной работы., например сFunctionKeys + PickПолучить поле функции типа в интерфейсе.

Кроме того, на самом деле некоторые из вышеперечисленных типов инструментов можно реализовать более лаконично и изящно с возможностями переназначения, почему бы не попробовать?

Ограниченный пространством (эта статья уже достигла 1,3w слов), тип инструмента type-fest, который я хотел выставить, можно только пожалеть, но я все же рекомендую всем прочитать его исходный код. По сравнению с вышеперечисленными типами утилит, он более обоснован, а идеи реализации более интересны.

Некоторые из новых функций в TypeScript 4.x

Эта часть является новой частью по сравнению с предыдущей версией и в основном включает в себя часть новых функций, представленных в версии 4.1–4.4 (бета-версия), которые связаны с содержимым, представленным в этой статье, включая тип литерала шаблона и переназначение.

Литеральные типы шаблонов

TypeScript 4.1Типы шаблонных литералов представлены в , так что мы можем использовать${}Этот синтаксис используется для построения литеральных типов, таких как:

type World = 'world';

// "hello world"
type Greeting = `hello ${World}`;

Литеральные типы шаблонов также поддерживают распределенные условные типы, например:

export type SizeRecord<Size extends string> = `${Size}-Record`

// "Small-Record"
type SmallSizeRecord = SizeRecord<"Small">
// "Middle-Record"
type MiddleSizeRecord = SizeRecord<"Middle">
// "Huge-Record"
type HugeSizeRecord = SizeRecord<"Huge">


// "Small-Record" | "Middle-Record" | "Huge-Record"
type UnionSizeRecord = SizeRecord<"Small" | "Middle" | "Huge">

Еще один интересный момент, шаблонные слоты (${}) можно передать в типе объединения, и если в одном шаблоне есть несколько слотов, каждый тип объединения будет организован и объединен отдельно.

// "Small-Record" | "Small-Report" | "Middle-Record" | "Middle-Report" | "Huge-Record" | "Huge-Report"
type SizeRecordOrReport = `${"Small" | "Middle" | "Huge"}-${"Record" | "Report"}`;

В нем представлены четыре новых типа инструментов:

type Uppercase<S extends string> = intrinsic;

type Lowercase<S extends string> = intrinsic;

type Capitalize<S extends string> = intrinsic;

type Uncapitalize<S extends string> = intrinsic;

Их роль буквально, без объяснений. Смотрите соответствующий PR40336, автор Андерс Хейлсберг — главный архитектор C# и Delphi, а также один из авторов TS.

intrinsicЭто означает, что эти типы инструментов реализованы внутри компилятора TS.На самом деле, хорошо известно, что мы не можем изменить значение литералов через программирование типов, но я думаю, что в соответствии с этой тенденцией программирование типов TS будет поддерживать вызов методов Lodash в будущее неопределенное.

Код реализации ТС:

function applyStringMapping(symbol: Symbol, str: string) {
 switch (intrinsicTypeKinds.get(symbol.escapedName as string)) {
     case IntrinsicTypeKind.Uppercase: return str.toUpperCase();
     case IntrinsicTypeKind.Lowercase: return str.toLowerCase();
     case IntrinsicTypeKind.Capitalize: return str.charAt(0).toUpperCase() + str.slice(1);
     case IntrinsicTypeKind.Uncapitalize: return str.charAt(0).toLowerCase() + str.slice(1);
 }
 return str;
}

Вы можете подумать, а что, если вы хотите перехватить часть литерала шаблона? Здесь нет возможности вызвать метод slice. На самом деле, идея заключается в выводе, о котором мы упоминали выше.После использования infer, чтобы занять место, вы можете извлечь часть литерала, например:

type CutStr<Str extends string> = Str extends `${infer Part}budu` ? Part : never

// "lin"
type Tmp = CutStr<"linbudu">

на шаг впереди,[1,2,3]такая строка, если мы предоставим[${infer Member1}, ${infer Member2}, ${infer Member}]Такое сопоставление слотов может привести к волшебному эффекту извлечения элементов строкового массива:

type ExtractMember<Str extends string> = Str extends `[${infer Member1}, ${infer Member2}, ${infer Member3}]` ? [Member1, Member2, Member3] : unknown;

// ["1", "2", "3"]
type Tmp = ExtractMember<"[1, 2, 3]">

Обратите внимание, что здесь используется слот шаблона,Разделенные, если несколько слотов с инфером находятся рядом друг с другом, предыдущий инфер получит только один символ, а последний инфер получит все оставшиеся символы (если они есть), например, мы ставим выше. Изменить пример на этот :

type ExtractMember<Str extends string> = Str extends `[${infer Member1}${infer Member2}${infer Member3}]` ? [Member1, Member2, Member3] : unknown;

// ["1", ",", " 2, 3"]
type Tmp = ExtractMember<"[1, 2, 3]">

Это свойство позволяет использовать множество смежных слотов infer+, чтобы вывести значение последней полученной рекурсии, например:

type JoinArrayMember<T extends unknown[], D extends string> =
  T extends [] ? '' :
  T extends [any] ? `${T[0]}` :
  T extends [any, ...infer U] ? `${T[0]}${D}${JoinArrayMember<U, D>}` :
  string;

// ""
type Tmp1 = JoinArrayMember<[], '.'>;
// "1"
type Tmp3 = JoinArrayMember<[1], '.'>;
// "1.2.3.4"
type Tmp2 = JoinArrayMember<[1, 2, 3, 4], '.'>;

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

Или наоборот? Пучок1.2.3.4Вернуться к форме массива?

type SplitArrayMember<S extends string, D extends string> =
  string extends S ? string[] :
  S extends '' ? [] :
  S extends `${infer T}${D}${infer U}` ? [T, ...SplitArrayMember<U, D>] :
  [S];

type Tmp11 = SplitArrayMember<'foo', '.'>;  // ['foo']
type Tmp12 = SplitArrayMember<'foo.bar.baz', '.'>;  // ['foo', 'bar', 'baz']
type Tmp13 = SplitArrayMember<'foo.bar', ''>;  // ['f', 'o', 'o', '.', 'b', 'a', 'r']
type Tmp14 = SplitArrayMember<any, '.'>;  // stri

Наконец, см.a.b.cВ таком виде следует думать о методе get Лодаша, то есть черезget({},"a.b.c")Форма быстро получает вложенные свойства. Но как вы предоставляете объявления типов таким образом? Если у вас есть литеральный тип шаблона, вам просто нужно объединить infer + условный тип.

type PropType<T, Path extends string> =
    string extends Path ? unknown :
    Path extends keyof T ? T[Path] :
    Path extends `${infer K}.${infer R}` ? K extends keyof T ? PropType<T[K], R> : unknown :
    unknown;

declare function getPropValue<T, P extends string>(obj: T, path: P): PropType<T, P>;
declare const s: string;

const obj = { a: { b: {c: 42, d: 'hello' }}};
getPropValue(obj, 'a');  // { b: {c: number, d: string } }
getPropValue(obj, 'a.b');  // {c: number, d: string }
getPropValue(obj, 'a.b.d');  // string
getPropValue(obj, 'a.b.x');  // unknown
getPropValue(obj, s);  // unknown

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

Эта способность находится вTS 4.1Представленный в , предоставляет возможность перенаправить источник сопоставления на новый тип в типе сопоставления, где новый тип может быть возвращаемым результатом типа инструмента, литерального типа шаблона и т. д., чтобы решить проблему использования сопоставления тип, мы хотим отфильтровать/добавить скопированный член интерфейса, обычно ключ исходного члена интерфейса используется в качестве нового параметра метода преобразования, например:

type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};

interface Person {
    name: string;
    age: number;
    location: string;
}

type LazyPerson = Getters<Person>;

Преобразованный результат:

type LazyPerson = {
    getName: () => string;
    getAge: () => number;
    getLocation: () => string;
}

здесьstring & kэто из-за метода преобразования переназначения (т.е.asболее поздняя часть) должна быть назначенаstring | number | symbol, а К происходит отkeyof, который может содержатьsymbolтип, который не может использоваться литеральными типами шаблона.

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

type RemoveKindField<T> = {
    [K in keyof T as Exclude<K, "kind">]: T[K]
};

interface Circle {
    kind: "circle";
    radius: number;
}

// type KindlessCircle = {
//     radius: number;
// }
type KindlessCircle = RemoveKindField<Circle>;

Наконец, при использовании с литералами шаблона, из-за их свойств перестановки, если метод преобразования переназначения является типом объединения, состоящим из типов литералов шаблона, тогда из перестановки будет получено несколько членов.

type DoubleProp<T> = { [P in keyof T & string as `${P}1` | `${P}2`]: T[P] }
type Tmp = DoubleProp<{ a: string, b: number }>;  // { a1: string, a2: string, b1: number, b2: number }

конец

Эта статья действительно очень длинная, потому не рекомендуется читать ее всю сразу, рекомендуется выделить несколько абзацев непрерывного времени определенной длины, разбить на части, разбить и хорошо прочитать. Писать статьи непросто, особенно с такой длинной статьей, но если это поможет вам поднять ваш TypeScript на новый уровень, оно того стоит.

Если вы никогда раньше не обращали внимания на аспект программирования типов, может потребоваться некоторое время, чтобы привыкнуть к изменению мнения после прочтения. Опять же, осознайтеСуть типового программирования заключается в программировании. Конечно, вы также можете начать практиковать это постепенно, например, начиная с сегодняшнего дня, начиная с текущего проекта, от дженериков до защиты типов, от индексных/сопоставленных типов до условных типов, от использования типов инструментов до инкапсуляции типов инструментов, шаг за шагом. стать мастером TypeScript.