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

Новые возможности TypeScript 4.1

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

  • Типы литералов шаблона
  • Переназначение клавиш в сопоставленных типах
  • Рекурсивные условные типы

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

TypeScript поддерживает типы строковых литералов с 1.8.

type Beatles = "John" | "Paul" | "George" | "Ringo"

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

type StoryPoints = 1 | 2 | 3 | 5 | 8 | 13 | 21 | 'Infinity';

В 4.1 TypeScript теперь поддерживает шаблонные литералы как типы. Если обычные литералы шаблона встраивают выражения в строку, типы литералов шаблона позволяют встраивать типы.

type Suit =
  "Hearts" | "Diamonds" |
  "Clubs" | "Spades";
type Rank =
  "Ace" | "Two" | "Three" | "Four" | "Five" |
  "Six" | "Seven" | "Eight" | "Nine" | "Ten" |
  "Jack" | "Queen" | "King";
type Card = `${Rank} of ${Suit}`;
const validCard: Card = "Three of Hearts";
const invalidCard: Card = "Three of Heart"; // Compiler Error

Типом Card будет каждая перестановка Suit и Rank в шаблоне ${Rank} of ${Suit}. Как видите, это полезно для построения шаблонов строковых типов. Это также может быть полезно при сочетании с другими операторами типов, такими как keyof.

interface UserData {
  name: string;
  age: number;
  registered: boolean;
}
// Generates: "getName" | "getAge" | "getRegistered"
type UserDataAccessorNames = `get${Capitalize<keyof UserData>}`;

Как видите, строковый тип был написан с заглавной буквы - TypeScript 4.1 добавил 4 таких помощника,

  • Capitalize<T>
  • Uncapitalize<T>
  • Uppercase<T>
  • Lowercase<T>

Переназначение клавиш в сопоставленных типах

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

type UserDataAccessors = {
  [K in keyof UserData]: () => UserData[K];
}

Это создает новый тип с исходными элементами данных, сопоставленными с функцией, возвращающей их тип:

{
  name: () => string;
  age: () => number;
  registered: () => boolean;
}

Теперь нам нужно сопоставить ключи типа `get${Capitalize<keyof UserData>}`. Мы могли бы сделать это просто как [K in `get${Capitalize<keyof UserData>}`], но нам нужно сохранить исходный K, чтобы получить доступ к UserDataAccessors[K]. Переназначение с помощью as позволяет нам отображать левую часть, сохраняя доступ к исходному ключу:

type UserDataAccessors = {
  [K in keyof UserData as `get${Capitalize<K>}`]: () => UserData[K];
}

Это создает тип:

{
  getName: () => string;
  getAge: () => number;
  getRegistered: () => boolean;
}

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

Последняя функция 4.1, о которой я собираюсь поговорить в этом посте, связана с всеми любимой темой программного обеспечения - рекурсией.

Рекурсивные условные типы - это именно то, что следует из названия, условные типы, которые ссылаются на себя.

type BuildTuple<Current extends [...T[]], T, Count extends number> =
  Current["length"] extends Count
    ? Current
    : BuildTuple<[T, ...Current], T, Count>
type Tuple<T, Count extends number> = BuildTuple<[], T, Count>

В этом примере создается тип Tuple, который будет генерировать кортеж указанного размера. Например:

// Generates: [string, string, string, string, string]
type StringQuintuple = Tuple<string, 5>

Давайте разберемся, что происходит на этом примере.

type BuildTuple<Current extends [...T[]], T, Count extends number> =

Мы можем думать об этом типе как о рекурсивной функции с параметрами типа параметры функции. Здесь важно помнить, что мы имеем дело с типами, а не значениями. Параметр Current - это строящийся кортеж, T - это тип каждого элемента в кортеже, а Count - необходимое количество записей. Обратите внимание на то, как Current ограничен как вариативный кортеж.

type BuildTuple<Current extends [...T[]], T, Count extends number> =
  Current["length"] extends Count
    ? Current
    : BuildTuple<[T, ...Current], T, Count>

Итак, если кортеж Current имеет правильную «длину», мы закончим и оценим как Current. В противном случае мы снова вызываем тип с расширенным кортежем. Опять же, обратите внимание, как новый Current генерируется с помощью операции распространения на предыдущем Current - [T, ...Current].

У нас уже были рекурсивные типы в TypeScript, но эта версия позволяет нам использовать их непосредственно в условных типах.

Безумные примеры

Если вы хотите увидеть несколько забавных примеров, я создал несколько репозиториев на GitHub. Эти примеры «вычисляют» тип, который является решением проблемы. Обратите внимание, что компилятор будет генерировать ошибки, поскольку происходит слишком много рекурсии, но это просто немного безумного удовольствия.

Судоку

https://github.com/eamonnboyle/sudoku-type-solver

Здесь у нас есть код, который генерирует тип, решающий головоломку Судоку.

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

type FilterSet<C extends Numbers, F extends Numbers> =
  IsSingleNumber<C> extends "true" ? C : Exclude<C, F>;

Я не использовал логические true и false, поскольку объединение true | false упрощается до boolean, и мне нужны различные значения.

Основной тип решателя использует рекурсию:

export type SolveGame<G extends SudokuGame> =
  IsGameComplete<G> extends "true" 
    ? G 
    : SolveGame< SolverIteration<G> >;

12 дней Рождества

https://github.com/eamonnboyle/12-days-of-christmas-type-solver

Этот проект создает текст классической праздничной песни 12 дней Рождества.

Он интенсивно использует строковые литералы, включая типы литералов шаблона:

const DaysTuple = [
    'Twelfth', 'Eleventh', 'Tenth', 'Ninth', 'Eighth', 'Seventh', 
    'Sixth', 'Fifth', 'Fourth', 'Third', 'Second', 'First',
] as const;
type DaysTupleType = typeof DaysTuple
type Days = DaysTupleType[number];
type FirstLine<D extends Days> = `On the ${D} day of Christmas my true love sent to me,`

Обратите внимание, как создается объединение Days с помощью утверждения const. Иногда это может быть полезно, когда вы хотите иметь единый источник истины для нескольких сущностей, таких как данные, тип кортежа и тип объединения.

Главный решатель снова использует рекурсию:

export type TwelveDaysOfChristmas<
  D extends readonly [...Days[]] = DaysTupleType,
  G extends [...Gifts[]] = GiftsTupleType
> =
    D["length"] extends 0
    ? []
    : [
        ...TwelveDaysOfChristmas<Tail<D>, Tail<G>>, 
        DayVerse<D[0], GiftsForDay<D, G>>
      ]

Заключение

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

Эта статья была изначально размещена здесь.