TypeScript Литеральные типы

Литеральные типы в TypeScript позволяют указывать точные значения, которые могут храниться в переменных, обеспечивая большую точность по сравнению с широкими типами, такими как string или number.

Они служат основой для создания чётких и защищённых типов приложений.


Ключевые концепции

  • Строковые литералы: Точные строковые значения, например, "success" | "error"
  • Числовые литералы: Конкретные числовые значения, например, 1 | 2 | 3
  • Булевые литералы: Только true или false
  • Шаблонные литеральные типы: Строковые литеральные типы, созданные с использованием синтаксиса шаблонов строк

Распространённые сценарии использования

  • Определение конкретных наборов допустимых значений
  • Создание дискриминантных объединений
  • Типобезопасная обработка событий
  • Типизация ответов API
  • Объекты конфигурации

Строковые литеральные типы

Строковый литеральный тип представляет точное строковое значение:

Пример


// Переменная со строковым литеральным типом
let direction: "north" | "south" | "east" | "west";

// Допустимые присваивания
direction = "north";
direction = "south";

// Недопустимые присваивания вызвали бы ошибки
// direction = "northeast"; // Ошибка: Тип '"northeast"' не может быть присвоен типу '"north" | "south" | "east" | "west"'
// direction = "up"; // Ошибка: Тип '"up"' не может быть присвоен типу '"north" | "south" | "east" | "west"'

// Использование строкового литерального типа в функциях
function move(direction: "north" | "south" | "east" | "west") {
  console.log(`Перемещение в направлении ${direction}`);
}

move("east"); // Правильно
// move("up"); // Ошибка: Аргумент типа '"up"' не может быть присвоен параметру типа...


Числовые литеральные типы

Подобно строковым литералам, числовые литеральные типы представляют конкретные числовые значения:

Пример


// Переменная с числовым литеральным типом
let diceRoll: 1 | 2 | 3 | 4 | 5 | 6;

// Допустимые присваивания
diceRoll = 1;
diceRoll = 6;

// Недопустимые присваивания вызвали бы ошибки
// diceRoll = 0; // Ошибка: Тип '0' не может быть присвоен типу '1 | 2 | 3 | 4 | 5 | 6'
// diceRoll = 7; // Ошибка: Тип '7' не может быть присвоен типу '1 | 2 | 3 | 4 | 5 | 6'
// diceRoll = 2.5; // Ошибка: Тип '2.5' не может быть присвоен типу '1 | 2 | 3 | 4 | 5 | 6'

// Использование числового литерального типа в функциях
function rollDice(): 1 | 2 | 3 | 4 | 5 | 6 {
  return Math.floor(Math.random() * 6) + 1 as 1 | 2 | 3 | 4 | 5 | 6;
}

const result = rollDice();
console.log(`Вы выбросили ${result}`);


Булевые литеральные типы

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

Пример


// Тип, который может быть только литеральным значением 'true'
type YesOnly = true;

// Функция, которая должна вернуть true
function alwaysSucceed(): true {
  // Всегда возвращает литеральное значение 'true'
  return true;
}

// Объединение булевого литерала с другими типами
type SuccessFlag = true | "success" | 1;
type FailureFlag = false | "failure" | 0;

function processResult(result: SuccessFlag | FailureFlag) {
  if (result === true || result === "success" || result === 1) {
    console.log("Операция выполнена успешно");
  } else {
    console.log("Операция завершилась неудачно");
  }
}

processResult(true); // "Операция выполнена успешно"
processResult("success"); // "Операция выполнена успешно"
processResult(1); // "Операция выполнена успешно"
processResult(false); // "Операция завершилась неудачно"


Объектные типы с литералами

Литеральные типы могут комбинироваться с объектными типами для создания чрезвычайно специфичной формы:

Пример


// Объект с литеральными свойствами
type HTTPSuccess = {
  status: 200 | 201 | 204;
  statusText: "OK" | "Created" | "No Content";
  data: any;
};

type HTTPError = {
  status: 400 | 401 | 403 | 404 | 500;
  statusText: "Bad Request" | "Unauthorized" | "Forbidden" | "Not Found" | "Internal Server Error";
  error: string;
};

type HTTPResponse = HTTPSuccess | HTTPError;

function handleResponse(response: HTTPResponse) {
  if (response.status >= 200 && response.status < 300) {
    console.log(`Успех: ${response.statusText}`);
    console.log(response.data);
  } else {
    console.log(`Ошибка ${response.status}: ${response.statusText}`);
    console.log(`Сообщение: ${response.error}`);
  }
}

// Пример использования
const successResponse: HTTPSuccess = {
  status: 200,
  statusText: "OK",
  data: { username: "john_doe", email: "john@example.com" }
};

const errorResponse: HTTPError = {
  status: 404,
  statusText: "Not Found",
  error: "Пользователь не найден в базе данных"
};

handleResponse(successResponse);
handleResponse(errorResponse);


Шаблонные литеральные типы

Начиная с TypeScript 4.1+, были введены шаблонные литеральные типы, которые позволяют создавать новые строковые литеральные типы путем комбинации существующих с использованием синтаксиса шаблонов строк:

Пример


// Базовые шаблонные литералы
type Direction = "north" | "south" | "east" | "west";
type Distance = "1km" | "5km" | "10km";

// Использование шаблонных литералов в сочетании друг с другом
type DirectionAndDistance = `${Direction}-${Distance}`;
// "north-1km" | "north-5km" | "north-10km" | "south-1km" | ...

let route: DirectionAndDistance;
route = "north-5km"; // Правильно
route = "west-10km"; // Правильно
// route = "north-2km"; // Ошибка
// route = "5km-north"; // Ошибка

// Продвинутая манипуляции со строками
type EventType = "click" | "hover" | "scroll";
type EventTarget = "button" | "link" | "div";
type EventName = `on${Capitalize<EventType>}${Capitalize<EventTarget>}`;
// "onClickButton" | "onClickLink" | "onClickDiv" | ...

// Доступ к динамическим свойствам
type User = {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
};

type GetterName<T> = `get${Capitalize<string & keyof T>}`;
type UserGetters = {
  [K in keyof User as GetterName<User>]: () => User[K];
};
// { getId: () => number; getName: () => string; ... }

// Работа с шаблоном строк
type ExtractRouteParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractRouteParams<Rest>
    : T extends `${string}:${infer Param}`
      ? Param
    : never;

type Params = ExtractRouteParams<"/users/:userId/posts/:postId">; // "userId" | "postId"

// Единицы измерения CSS и значения
type CssUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
type CssValue = `${number}${CssUnit}`;

let width: CssValue = '100px'; // Правильно
let height: CssValue = '50%'; // Правильно
// let margin: CssValue = '10'; // Ошибка
// let padding: CssValue = '2ex'; // Ошибка

// Версии API
type ApiVersion = 'v1' | 'v2' | 'v3';
type Endpoint = 'users' | 'products' | 'orders';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

type ApiUrl = `https://api.example.com/${ApiVersion}/${Endpoint}`;

// Сложный пример: Динамический генератор SQL-запросов
type Table = 'users' | 'products' | 'orders';
type Column<T extends Table> =
  T extends 'users' ? 'id' | 'name' | 'email' | 'created_at' :
  T extends 'products' ? 'id' | 'name' | 'price' | 'in_stock' :
  T extends 'orders' ? 'id' | 'user_id' | 'total' | 'status' : never;

type WhereCondition<T extends Table> = {
  [K in Column<T>]?: {
    equals?: any;
    notEquals?: any;
    in?: any[];
  };
};

function query<T extends Table>(
  table: T,
  where?: WhereCondition<T>
): `SELECT * FROM ${T}${string}` {
  // Реализация строила бы запрос
  return `SELECT * FROM ${table}` as const;
}

// Использование
const userQuery = query('users', {
  name: { equals: 'John' },
  created_at: { in: ['2023-01-01', '2023-12-31'] }
});
// Тип: "SELECT * FROM users WHERE ..."


Лучшие практики

Да:

  • Используйте литеральные типы для фиксированных наборов значений (перечисления, варианты конфигурации)
  • Комбинируйте с объединёнными типами для повышения уровня типобезопасности
  • Используйте шаблонные литеральные типы для сопоставления строковых паттернов
  • По возможности полагайтесь на автоматический вывод типов
  • Документально поясняйте смысл литеральных типов

Нет:

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

Вопросы производительности

Производительность проверки типов

  • Большие объединения типов могут замедлять проверку типов
  • Сложные шаблонные литеральные типы увеличивают время компиляции
  • Рассмотрите возможность использования псевдонимов типов для сложных литеральных типов
  • Учитывайте ограничения глубины рекурсии TypeScript