Что такое типы литералов шаблонов?

TypeScript 4.1 представил новую интересную функцию, называемую литеральными типами шаблонов . Эта функция позволяет разработчикам использовать литеральный синтаксис шаблона не только в значениях, но и в типах. Синтаксис литерала шаблона упрощает вставку значений в строки в удобочитаемой и простой форме за счет использования обратных кавычек для разделения выражений, окруженных ${…}.

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

type Direction = 'left' | 'right' | 'up' | 'down';

// Only 'go-left' | 'go-right' | 'go-up' | 'go-down' is allowed
type DirectionCommand = `go-${Direction}`

// ✅ Compiles successfully!
const goodDirection: DirectionCommand = 'go-right';

// 🛑 Compiler error: Type '"go-somewhere"' is not assignable to type '"go-left" | "go-right" | "go-up" | "go-down"'.
const badDirection: DirectionCommand = 'go-somewhere'; 

Как разработчики могут извлечь выгоду из этой функции?

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

// Each constant represent error codes translated in different languages
// Starts with an `error` string 
// followed by a dash sign `-` and an error code number
// then we have a `:` and a standard ISO language code
const error1 = 'error-134:en';
const error2 = 'error-3:es';
const error3 = 'error-4:ru';

function extractErrorCode(error: string): number {
  if (error.length < 0) {
    throw Error('error is empty');
  }

  if (!error.startWith('error')) {
    throw Error('Invalid');
  }

  const errorCodeString = error.substring(
      error.indexOf("-") + 1, 
      error.indexOf(":")
  );
  return parseInt(errorCodeString);
}

function extractLanguageCode(error: string): string {
  if (error.length < 0) {
    throw Error('error is empty');
  }

  if (!error.startWith('error')) {
    throw Error('Invalid');
  }

  return error.substring(error.indexOf(":") + 1);
}

console.log(extractErrorCode(error1)); // Result: 134
console.log(extractErrorCode(error2)); // Result: 3
console.log(extractErrorCode(error3)); // Result: 4

console.log(extractLanguageCode(error1)); // Result: en
console.log(extractLanguageCode(error2)); // Result: es
console.log(extractLanguageCode(error3)); // Result: ru

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

  • Как вы можете гарантировать, что аргумент функции будет соответствовать соглашению? Возможное решение — выдать ошибку, если аргумент не соответствует регулярному выражению . Большинство разработчиков считают регулярные выражения трудными для понимания, и я их не виню.
  • Что произойдет, если соглашение изменится? Сколько времени вы потратите на изменение каждой функции, чтобы она соответствовала новым правилам?
  • Кто-то должен поддерживать более 15 строк логического кода. Можем ли мы сделать его проще и надежнее? Ответ ДА, но мы должны использовать правильные инструменты.

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

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

// Union between multiple language code strings
type LanguageCode = 'en' | 'es' | 'ru'; 

// Error pattern definition using Template Literal Types
type TranslatedError = `error-${number}:${LanguageCode}`;

// ✅
const error1: TranslatedError = 'error-134:en'; 
const error2: TranslatedError = 'error-3:es'; 
const error3: TranslatedError = 'error-4:ru'; 

// 🛑 ... not assignable to type ...
const error4: TranslatedError = 'error-something:es'; // wrong number
const error5: TranslatedError = 'fault-4:en'; // wrong pattern
const error6: TranslatedError = 'error-3:english' // wrong language code

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

type ErrorCode<T extends string> = T extends `error-${infer E}:${LanguageCode}` ? E : never;
type ErrorCode13 = ErrorCode<'error-13:es'>; // Type: '13'

Вы также можете манипулировать литералами шаблонов, используя некоторые определенные типы, такие как Capitalize, Uncapitalize, Uppercase, Lowercase:

type VerticalPosition = 'top' | 'bottom';
type HorizontalPosition = 'left' | 'right';
type CombinedPosition = `${VerticalPosition}${Capitalize<HorizontalPosition>}`;
type WidgetPosition = VerticalPosition | HorizontalPosition | CombinedPosition;

// ✅
const top: WidgetPosition = 'top'; 
const topRight: WidgetPosition = 'topRight';
const bottom: WidgetPosition = 'bottom'

// 🛑
const middle: WidgetPosition = 'topBottom'; // Compile error!

Заключение

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