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

1. Аннотация типа

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

1.1 Синтаксические конструкции var, let, const

При объявлении синтаксических конструкций объявляемых с помощью операторов var , let и const , тип данных указывается сразу после идентификатора.

var identifier: Type = value;
let identifier: Type = value;
const IDENTIFIER: Type = value;

1.2 Функции (function)

При объявлении функции тип возвращаемого ею значения указывается между её параметрами и телом. При наличии параметров, тип данных указывается и для них.

function identifier(param1: Type, param2: Type): ReturnedType {
  // Body
}

// Реальный пример
function add (a: number, b: number): number {
  return a + b
}

const result = add(5, 5) // result: number

Помимо этого, в TypeScript можно объявлять параметризированные функции. Функции, имеющие параметры типа, называются обобщенными (подробнее о них речь пойдет далее). Параметры типа заключаются в угловые скобки <> и располагаются перед круглыми скобками () , в которые заключены параметры функции.

function identifier <T, U>(): ReturnedType {
  // Body
}

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

function identifier(p1: T1, p2: T2): T3;
function identifier(p1: T4, p2: T5): T6;

function identifier(p1: T, p2: T): T {
  return 'value';
}

const a: T1 = 'value';
const b: T2 = 'value';
const c: T4 = 'value';
const d: T5 = 'value';

identifier(a, b); // валидно
identifier(c, d); // валидно
class Identifier {
  constructor(p1: T1, p2: T2);
  constructor(p1: T4, p2: T5);
  
  constructor(p1: T, p2: T) {
    identifier(p1: T1, p2: T2): T3;
    identifier(p1: T4, p2: T5): T6;
    
    identifier(p1: T, p2: T): T {
      return 'value';
    }
  }
}

1.3 Стрелочные Функции (arrow function)

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

(param: Type, param2: Type): Type => value;

<T, U>(param: Type, param2: Type): Type => value;

1.4 Классы (class)

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

Для свойств, в частности для get , указывается тип данных возвращаемого значения. Для set указывается лишь тип единственного параметра, а возвращаемый им тип и вовсе запрещается указывать явно. Кроме того, классы в TypeScript также могут быть обобщенными. В случае объявления обобщенного класса, параметры типа, заключенные в треугольные скобки, указываются сразу после идентификатора класса.

class Identifier<T> {
  static staticField: Type = value; // член класса
  
  static get staticProperty(): Type { // член класса
    return value;
  }
  
  static set staticProperty(value: Type) { // член класса
  }

  [indexSignature: Type]: Type; // член класса

  [computedProp]: Type = value; // член класса

  field: Type = value; // член класса

  field: Type = value; // член класса

  get property(): Type { // член класса
    return value;
  }

  set property(value: Type) { // член класса
  }
}
class Identifier<T> {
  static staticMethod <T, U>(param0: Type, param1: Type): Type { // член класса
  }

  static { // статический блок
  }

  constructor(param0: Type, param1: Type) {
  }

  method <T, U>(param0: Type, param1: Type): Type { // член класса
  }
}

2. Типы данных

Типы данных TS включают в себя следующие виды: Number, String, Boolean, Symbol, BigInt, any, Null, Undefined, Void, Never, Unknown, Enum, Union, Intersection, Object, Array, Tuple, Function.

2.1 Базовый Тип Any

Тип any указывается при помощи ключевого слова any . Все типы в TypeScript являются его подтипами. Это означает, что он совместим в обе стороны с любым другим типом и с точки зрения системы типов является высшим типом (top type).

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

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

Если тип не указан и TypeScript не в состоянии его вывести из контекста, то компилятор обычно по умолчанию использует any.

2.2 Unknown (unknown)

Тип Unknown является типобезопасным аналогом типа any и представлен в виде литерала unknown . Все типы совместимы с типом unknown , в, то время как сам тип unknown совместим только с самим собой и типом any .

Тип unknownпредставляет любое значение. Он похож на anyтип , но безопаснее, поскольку со значением запрещено что-либо делать.

function f1 (a: any) {
  a.b() // OK
}

function f2 (a: unknown) {
  a.b() // Property 'b' does not exist on type 'unknown'
}

И наоборот, можно описать функцию, которая возвращает значение неизвестного типа:

function safeParse (s: string): unknown {
  return JSON.parse(s)
}

// Need to be careful with 'obj'!
const obj = safeParse(someRandomString)

Над типом unknown запрещено выполнение каких-либо операций.

let v0: any

v0.a = 5 // Ok
v0.a = '' // Ok
v0() // Ok

//////////////////////

const v1: unknown = v0 // Ok

v1.a = 5 // Error Property 'a' does not exist on type 'unknown'
v1.a = '' // Error Property 'a' does not exist on type 'unknown'
v1() // Error This expression is not callable. Type '{}' has no call signatures

Если тип unknown составляет тип пересечение ( intersection ), то он будет перекрыт всеми типами.

type T0 = any & unknown; // type T0 = any
type T1 = number & unknown; // type T1 = number
type T2 = string & unknown; // type T2 = string
type T3 = boolean & unknown; // type T3 = boolean
type T4 = null & unknown; // type T4 = null
type T5 = undefined & unknown; // type T5 = undefined
type T6 = void & unknown; // type T6 = void
type T7 = never & unknown; // type T7 = never
type T8<T> = T & unknown; // type T8 = T
type T9 = unknown & unknown; // type T9 = unknown

Если тип unknown составляет тип объединение ( union ), то он перекроет все типы, за исключением типа any.

type T0 = any | unknown; // type T0 = any
type T1 = number | unknown; // type T1 = unknown
type T2 = string | unknown; // type T2 = unknown
type T3 = boolean | unknown; // type T3 = unknown
type T4 = null | unknown; // type T4 = unknown
type T5 = undefined | unknown; // type T5 = unknown
type T6 = void | unknown; // type T6 = unknown
type T7 = never | unknown; // type T7 = unknown
type T8<T> = T | unknown; // type T8 = unknown
type T9 = unknown | unknown; // type T9 = unknown

Запрос ключей ( keyof ) для типа unknown возвращает тип never

type T0 = keyof number; // type T0 = "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"
type T1 = keyof any; // type T1 = string | number | symbol
type T2 = keyof unknown; // type T2 = never

Тип unknown позволяется использовать только в операциях равенства === , == , !== и != и в операциях с логическими операторами && , || и ! .

let v0: unknown = 5
const v1 = 5 === v0 // Ok
const v2 = 5 !== v0 // Ok

const v3 = 5 > v0 // Error Оператор '>' нельзя применять к типам 'number' и 'unknown'.
const v4 = 5 < v0 // Error Оператор '<' нельзя применять к типам 'number' и 'unknown'.
const v5 = 5 >= v0 // Error Оператор '>=' нельзя применять к типам 'number' и 'unknown'.
const v6 = 5 <= v0 // Error Оператор '<=' нельзя применять к типам 'number' и 'unknown'.

const v7 = 5 - v0 // Error Правая часть арифметической операции должна иметь тип «any», «number», «bigint» или тип enum.
const v8 = 5 * v0 // Error Правая часть арифметической операции должна иметь тип «any», «number», «bigint» или тип enum.
const v9 = 5 / v0 // Error Правая часть арифметической операции должна иметь тип «any», «number», «bigint» или тип enum.

const v10 = ++v0 // Error Арифметический операнд должен иметь тип «any», «number», «bigint» или тип enum.
const v11 = --v0 // Error Арифметический операнд должен иметь тип «any», «number», «bigint» или тип enum.
const v12 = v0++ // Error Арифметический операнд должен иметь тип «any», «number», «bigint» или тип enum.
const v13 = v0-- // Error Арифметический операнд должен иметь тип «any», «number», «bigint» или тип enum.

const v14 = 5 && v0 // Ok, let v14: unknown Но неожиданная постоянная истинность в левой части выражения `&&`
const v15 = 5 || v0 // Ok, let v15: number Но неожиданная постоянная истинность в левой части выражения `||`.
const v16 = v0 || 5 // Ok, let v16: unknown
const v17 = !v0 // Ok, let v17: boolean

Функция у которой возвращаемый тип принадлежит к типу unknown , может не возвращать значение явно.

function f0 (): unknown {
  return // Ok
}

const v = f0() // Ok, let v: unknown

При активной опции --strictPropertyInitialization принадлежащие к типу unknown поля не нуждаются в инициализации.

class T {
  f0: unknown // Ok

  f1: number // Error

  f2: number = 5 // Ok
}

Если в определении типа данных участвует сопоставленный тип Mapped Type которому в качестве аргумента типа передается тип unknown , то такой сопоставленный тип будет выведен как объектный тип {} .

type MappedType<T> = {
    [K in keyof T]: T;
};
  
type T0 = MappedType<number>; // type T0 = number
type T1 = MappedType<any>; // type T1 = { [x: string]: any; }
type T2 = MappedType<unknown>; // type T2 = {}

2.3 Null (null) примитивный null тип

Примитивный тип Null служит обозначением “ничего”. Тип Null указывается с помощью ключевого слова null (не путать с единственным литеральным значением null типа Null , которое присваивается в качестве значения).

null, указанный после оператора двоеточия, это имеющийся только в TypeScript псевдоним (alias) для глобального типа Null. В, то время как null, указанный после оператора присваивания, это единственное значение типа Null.

let identifier: null = null

Тип Null является подтипом всех типов, за исключением типа Undefined, поэтому его единственное значение null совместимо со всеми остальными типами данных.

class TypeSystem {
  static any: any = null; // Ok
  static number: number = null; // Ok
  static string: string = null; // Ok
  static boolean: boolean = null; // Ok
  static undefined: undefined = null; // Ok
  static null: null = null; // Ok
}

В то время как тип null совместим со всеми типами, помимо него самого, с ним самим совместим лишь тип undefined и any.

TypeSystem.null = TypeSystem.any // Ok
TypeSystem.null = TypeSystem.number // Error Type 'number' is not assignable to type 'null'
TypeSystem.null = TypeSystem.string // Error Type 'string' is not assignable to type 'null'
TypeSystem.null = TypeSystem.boolean // Error Type 'boolean' is not assignable to type 'null'
TypeSystem.null = TypeSystem.undefined // Ok
TypeSystem.null = TypeSystem.null // Ok

Тогда, когда тип данных указывается не явно, а в качестве значения используется значение null, вывод типов определяет принадлежность к типу any (если не указхан --strictNullChecks).

const identifier = null // identifier: any

Создатели TypeScript во избежание ошибок, возникающих при операциях, в которых вместо ожидаемого значения возможно значение null, рекомендуют вести разработку с активным флагом --strictNullChecks. При активном флаге --strictNullChecks тип null является подтипом только одного типа any. Это в свою очередь означает, что значение null может быть совместимо только с типами any и null.

class TypeSystem {
  static any: any = null // Ok

  static number: number = null // Error

  static string: string = null // Error

  static boolean: boolean = null // Error

  static undefined: undefined = null // Error

  static null: null = null // Ok
}

TypeSystem.null = TypeSystem.any // Ok
TypeSystem.null = TypeSystem.number // Error
TypeSystem.null = TypeSystem.string // Error
TypeSystem.null = TypeSystem.boolean // Error
TypeSystem.null = TypeSystem.undefined // Error
TypeSystem.null = TypeSystem.null // Ok

//При активном флаге --strictNullChecks, при условии, что в качестве значения выступает значение null, вывод типов определяет принадлежность к типу null.
const identifier = null // identifier: null

2.4 Undefined (undefined) примитивный неопределенный тип

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

undefined, указанный после оператора двоеточия, это имеющийся только в TypeScript псевдоним (alias) для глобального типа Undefined. В, то время как undefined, указанный после оператора присваивания, это единственное значение типа Undefined.

let identifier: undefined = undefined

Во время выполнения объявленные, но не инициализированные переменные, поля и свойства класса, а также параметры имеют значение undefined . Также значение undefined является результатом вызова методов или функций, которые не возвращают значения.

Тип undefined является подтипом всех типов, что делает его совместимым со всеми остальными типами.

class TypeSystem {
    static any: any = undefined; // Ok
    static number: number = undefined; // Ok
    static string: string = undefined; // Ok
    static boolean: boolean = undefined; // Ok
    static null: null = undefined; // Ok
    static undefined: undefined = undefined; // Ok
  }

А с ним совместимы лишь null и any.

TypeSystem.undefined = TypeSystem.any; // Ok
TypeSystem.undefined = TypeSystem.number; // Error
TypeSystem.undefined = TypeSystem.string; // Error
TypeSystem.undefined = TypeSystem.boolean; // Error
TypeSystem.undefined = TypeSystem.null; // Ok
TypeSystem.undefined = TypeSystem.undefined; // Ok

Тогда, когда тип данных undefined указывается не явно, компилятор устанавливает
тип any .

let identifier = undefined; // identifier: any

При активном флаге --strictNullChecks, тип undefined является подтипом только одного типа any. Поэтому ему в качестве значения, помимо самого себя, можно присвоить только тип any.

class TypeSystem {
  static any: any = undefined; // Ok
  static number: number = undefined; // Error
  static string: string = undefined; // Error
  static boolean: boolean = undefined; // Error
  static null: null = undefined; // Error
  static undefined: undefined = undefined; // Ok
}
  
TypeSystem.undefined = TypeSystem.any; // Ok
TypeSystem.undefined = TypeSystem.number; // Error
TypeSystem.undefined = TypeSystem.string; // Error
TypeSystem.undefined = TypeSystem.boolean; // Error
TypeSystem.undefined = TypeSystem.null; // Error
TypeSystem.undefined = TypeSystem.undefined; // Ok

При активном флаге --strictNullChecks, при условии, что в качестве значения выступает значение undefined, вывод типов определяет принадлежность к типу undefined.

let identifier = undefined; // identifier: undefined

2.5 Void (void) отсутствие конкретного типа

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

Тип данных Void указывается с помощью ключевого слова void (не путать с одноимённым оператором из JavaScript) и, в отличие от таких типов, как null и undefined , не имеет никаких значений. Тип void является подтипом any и супертипом для null и undefined .

function action(): void {}

class TypeSystem {
  static any: any = action(); // Ok
  static number: number = action(); // Error
  static string: string = action(); // Error
  static boolean: boolean = action(); // Error
  static null: null = action(); // Error
  static undefined: undefined = action(); // Error
  static void: void = action(); // Ok
}

TypeSystem.void = TypeSystem.any; // Ok
TypeSystem.void = TypeSystem.number; // Error
TypeSystem.void = TypeSystem.string; // Error
TypeSystem.void = TypeSystem.boolean; // Error
TypeSystem.void = TypeSystem.null; // Ok (если не указан --strictNullChecks)
TypeSystem.void = TypeSystem.undefined; // Ok
TypeSystem.void = TypeSystem.void; // Ok

Однако с активным флагом --strictNullChecks , тип данных void совместим лишь с any и undefined .

function action(): void {}

class TypeSystem {
  static any: any = action(); // Ok
  static number: number = action(); // Error
  static string: string = action(); // Error
  static boolean: boolean = action(); // Error
  static null: null = action(); // Error
  static undefined: undefined = action(); // Error
  static void: void = action(); // Ok
}

TypeSystem.void = TypeSystem.any; // Ok
TypeSystem.void = TypeSystem.number; // Error
TypeSystem.void = TypeSystem.string; // Error
TypeSystem.void = TypeSystem.boolean; // Error
TypeSystem.void = TypeSystem.null; // Error
TypeSystem.void = TypeSystem.undefined; // Ok
TypeSystem.void = TypeSystem.void; // Ok

Когда функции в качестве возвращаемого типа указан тип void , может показаться, что возвращая различные значения с помощью оператора return , компилятор выбрасывает ошибки из-за понимания, что функция помечена как ничего не возвращающая. Но это не так. Ошибка возникает по причине несовместимости типов. Обратите внимание на сообщение об ошибке:

function a(): void {
  let result: number = 5

  return result // Type 'number' is not assignable to type 'void'
}

function b(): void {
  let result: string = ''

  return result // Type 'string' is not assignable to type 'void'
}

function c(): void {
  let result: any = 5

  return result // ok any совместим с void
}

function d(): void {
  return 5 as any // ok
}

function i(): void {
  return null as null // Ok (если не указан --strictNullChecks
}

Для функций и методов, которые ничего не возвращают и у которых отсутствует аннотация типа возвращаемого значения, вывод типов определяет принадлежность к типу void .

function action() {} // function action(): void

В отличие от null и undefined , тип void не имеет ни одного значения, которое могло бы явно продемонстрировать присвоение. Однако компилятор понимает, что имеет дело с типом void при вызове функции или метода, которые не возвращают значение. Этот становится ещё нагляднее, когда вывод типов устанавливает тип полученный при вызове функции или метода которые ничего не возвращают.

function action() {}

let identifier = action(); // identifier: void

2.6 Never (never) Никогда

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

function action(): never {
  throw new Error();
};

class TypeSystem {
  static any: any = action(); // Ok
  static number: number = action(); // Ok
  static string: string = action(); // Ok
  static boolean: boolean = action(); // Ok
  static null: null = action(); // Ok
  static undefined: undefined = action(); // Ok
  static void: void = action(); // Ok
  static never: never = action(); // Ok
}

Типу never нельзя присвоить значение, отличное от самого типа never.

TypeSystem.never = TypeSystem.any; // Error
TypeSystem.never = TypeSystem.number; // Error
TypeSystem.never = TypeSystem.string; // Error
TypeSystem.never = TypeSystem.boolean; // Error
TypeSystem.never = TypeSystem.null; // Error
TypeSystem.never = TypeSystem.undefined; // Error
TypeSystem.never = TypeSystem.void; // Error
TypeSystem.never = TypeSystem.never; // Ok

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

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

Буквально never переводится как "никогда" . Поэтому оно присваивается некоторой функции которая никогда не возвращают значение.

function error(message: string): never {
  throw new Error(message);
}

function loop(): never {
  while(true) {}
}

neverтакже появляется, когда TypeScript определяет, что в объединении union ничего не осталось.

2.7 Enum (enum) примитивный перечисляемый тип

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

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

Enum — это конструкция, состоящая из ��абора именованных констант, именуемая списком перечисления и определяемая такими примитивными типами, как number и string . Enum объявляется с помощью ключевого слова enum .

Идентификаторы-имена для перечислений enum принято задавать во множественном числе. В случае, когда идентификаторам констант значение не устанавливается явно, они ассоциируются с числовым значениями, в порядке возрастания, начиная с нуля.

enum Fruits {
  Apple, // 0
  Pear, // 1
  Banana // 2
}

Также можно установить любое значение вручную

enum Citrus {
  Lemon = 2, // 2
  Orange = 4, // 4
  Lime = 6 // 6
}

Если указать значение частично, то компилятор будет стараться соблюдать последовательность.

enum Berries {
  Strawberry = 1,
  Raspberry, // 2

  Blueberry = 4,
  Cowberry // 5
}

Компилятор рассчитывает значение автоматически только на основе значения предыдущего члена перечисления.

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

enum Keys {
  A = 10,
  B, // 11
  C = 10,
  D // 11
}

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

enum позволяет задавать псевдонимы (alias). Псевдонимам устанавливается значение константы, на которую они ссылаются.

enum Languages {
  Apple, // en, value = 0
  
  Apfel = Apple, // de, value = 0
  LaPomme = Apple // fr, value = 0
}

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

let value: number = Fruits.Apple; // 0
let identificator: string = Fruits[value]; // “Apple”

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

enum E {
  A = getSomeValue(),
  B, // Error Enum member must have initializer.
}

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

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

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

enum BooleanLikeHeterogeneousEnum {
  No = 0,
  Yes = "YES",
}

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

  • Это первый член перечисления, и у него нет инициализатора, в этом случае ему присваивается значение 0:

// E.X is constant:
enum E {
  X,
}
  • У него нет инициализатора, а предыдущий элемент перечисления был числовой константой. В этом случае значение текущего элемента перечисления будет равно значению предыдущего элемента перечисления плюс один:

// All enum members in 'E1' and 'E2' are constant.
 
enum E1 {
  X,
  Y,
  Z,
}
 
enum E2 {
  A = 1,
  B,
  C,
}
  • Член перечисления инициализируется константным выражением перечисления. Константное выражение перечисления — это подмножество выражений TypeScript, которое может быть полностью вычислено во время компиляции. Выражение является константным выражением перечисления, если оно:

  1. литеральное выражение перечисления (в основном строковый литерал или числовой литерал)

  2. ссылка на ранее определенный константный член перечисления (который может происходить из другого перечисления)

  3. константное выражение перечисления в скобках

  4. один из унарных операторов, +применяемых к константному выражению перечисления-~

  5. +-*/%<<>>>>>&, бинарные операторы с константными выражениями перечисления в качестве |операндов^

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

enum FileAccess {
  // constant members
  None,
  Read = 1 << 1,
  Write = 1 << 2,
  ReadWrite = Read | Write,
  // computed member
  G = "123".length,
}

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

  • любой строковый литерал (например "foo", , "bar""baz")

  • любой числовой литерал (например 1100)

  • унарный минус, применяемый к любому числовому литералу (например -1, , -100)

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

Во-первых, члены перечисления также становятся типами! Например, можно сказать, что некоторые члены могут иметь только значение члена перечисления:

enum ShapeKind {
  Circle,
  Square,
}
 
interface Circle {
  kind: ShapeKind.Circle;
  radius: number;
}
 
interface Square {
  kind: ShapeKind.Square;
  sideLength: number;
}
 
let c: Circle = {
  kind: ShapeKind.Square, // Error Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.
  radius: 100,
};

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

Например TS предупреждает что это сравнение, по-видимому, непреднамеренное, поскольку типы «E.Foo» и «E.Bar» не пересекаются.

enum E {
  Foo,
  Bar,
}
 
function f(x: E) {
  if (x !== E.Foo || x !== E.Bar) {
  // This comparison appears to be unintentional because the types 'E.Foo' and 'E.Bar' have no overlap.
  }
}

В этом примере мы сначала проверили , не равенx ли . Если проверка прошла успешно, то наше выражение замкнётся, и тело оператора if выполнится. Однако если проверка не прошла успешно, то может быть только , поэтому нет смысла проверять, не равен ли он . E.Foo || x E.Foo E.Bar

Перечисления — это реальные объекты, существующие во время выполнения. Например, следующее перечисление:

enum E {
  X,
  Y,
  Z,
}

на самом деле может быть передан функциям

enum E {
  X,
  Y,
  Z,
}
 
function f(obj: { X: number }) {
  return obj.X;
}
 
// Works, since 'E' has a property named 'X' which is a number.
f(E);

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

enum  Placement {
  top,
  left,
  right
}

const B: keyof typeof Placement = 'top' // const B: "top" | "left" | "right"

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

enum Enum {
  A,
}

const a: number = Enum.A // const a: number = 0
const nameOfA: string = Enum[a] // const nameOfA: string = 'A'

Таким образом перечисление компилируется в объект, хранящий как прямые ( name-> value), так и обратные ( value-> name) сопоставления. Ссылки на другие члены перечисления всегда создаются как обращения к свойствам и никогда не встраиваются.

Но для членов строкового перечисления обратное отображение вообще не создается.

В большинстве случаев перечисления являются вполне допустимым решением. Однако иногда требования более строгие. Чтобы избежать затрат на создание дополнительного кода и дополнительных косвенных адресов при доступе к значениям перечислений, можно использовать constперечисления. Константные перечисления определяются с помощью constмодификатора enum.

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

const enum Direction {
  Up,
  Down,
  Left,
  Right,
}
 
let directions: Record<Direction, number> = [
  Direction.Up,
  Direction.Down,
  Direction.Left,
  Direction.Right,
];

В сгенерированном файле получаем:

var directions = [
    0 /* Direction.Up */,
    1 /* Direction.Down */,
    2 /* Direction.Left */,
    3 /* Direction.Right */,
];

А для обычного перечисления мы бы имели следующий ход на выходе

var Direction;

(function (Direction) {
    Direction[Direction["Up"] = 0] = "Up";
    Direction[Direction["Down"] = 1] = "Down";
    Direction[Direction["Left"] = 2] = "Left";
    Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));

var directions = [
    Direction.Up,
    Direction.Down,
    Direction.Left,
    Direction.Right,
];

Как видно из примера разница в том, что обычное перечисление сохраняет обратную совместимость name-> value так и name-> value а константные перечисления имеют ТОЛЬКО совместимость name-> value Пример:

enum Direction {
  Up,
  Down,
}

// Получаем ЗНАЧЕНИЕ по его НАЗВАНИЮ name -> value
const a = Direction.Up // 0

// Получаем НАЗВАНИЕ по его ЗНАЧЕНИЮ value -> name
const b = Direction[0] // 'Up'

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

const enum Direction {
  Up,
  Down,
}

// Получаем ЗНАЧЕНИЕ по его НАЗВАНИЮ name -> value
const a = Direction.Up // 0

// Error A const enum member can only be accessed using a string literal
const b = Direction[0]

В современном TypeScript вам может не понадобиться перечисление, когда объекта с as constможет быть достаточно:

// По сути оба варианта эквивалентны

const enum EDirection {
  Up,
  Down,
  Left,
  Right,
}
 
const ODirection = {
  Up: 0,
  Down: 1,
  Left: 2,
  Right: 3,
} as const;

EDirection.Up; // (enum member) EDirection.Up = 0
 
ODirection.Up; // (property) Up: 0
 
// Использование перечисления в качестве параметра
function walk(dir: EDirection) {}
 
// Для извлечения значений требуется дополнительная строка.
type Direction = typeof ODirection[keyof typeof ODirection];
function run(dir: Direction) {}
 
walk(EDirection.Left);
run(ODirection.Right);

Если вы сомневаетесь использовать Enum, то советую прочесть статью: "TypeScript здорового человека, или почему с Enum лучше"