1. Object, Array, Tuple

1.1 Object (object) — ссылочный объектный тип

Ссылочный тип данных Object является базовым для всех ссылочных типов в TypeScript подобно тому как в JavaScript Object является прототипом всех остальных ссылочных типов.

Помимо того, что в TypeScript существует объектный тип Object , представляющий одноименный конструктор из JavaScript, также существует тип object , представляющий любое объектное значение. Поведение типа указанного с помощью ключевого слова object и интерфейса Object различаются.

Переменные, которым указан тип с помощью ключевого слова object , не могут хранить значения примитивных типов, чьи идентификаторы (имена) начинаются со строчной буквы ( number , string и т.д.). В отличие от них тип интерфейс Object совместим с любым типом данных. Возникает ошибка: Error: Type X is not assignable to type 'object' (Тип X не может быть назначен типу «объект»).

let o: object;
let O: Object;

o = 5; // Error: Type 'number' is not assignable to type 'object'
O = 5; // Ok

o = ''; // Error
O = ''; // Ok

o = true; // Error
O = true; // Ok

o = null; // Error, strictNullChecks = true
O = null; // Error, strictNullChecks = true

o = undefined; // Error, strictNullChecks = true
O = undefined; // Error, strictNullChecks = true

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

При попытке обратиться к членам объекта, незадекларированным в интерфейсе Object, возникнет ошибка.

class SeaLion {
    rotate(): void {}
}

let seaLionAsObject: object = new SeaLion()

seaLionAsObject.rotate() // Property 'rotate' does not exist on type 'object'

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

1.2 Array (type[]) ссылочный массивоподобный тип

Ссылочный тип данных Array является типизированным спископодобным объектом, содержащим логику для работы с элементами.Тип данных Array указывается с помощью литерала массива, перед которым указывается тип данных type[] .

Перед фигурными скобками [] указывается тип TypeScript, указывающий какие типы данных может хранить данный массив. Следующий пример массив строк:

let animalAll: string[] = [
  'Elephant',
  'Rhino',
  'Gorilla'
];

animalAll.push(5); // Error Argument of type 'number' is not assignable to parameter of type 'string'
animalAll.push('str'); // Ok

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

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

const someArr: (string | number)[] = [
  'str',
  10,
  20,
  'str'
]

В случаях, требующих создания экземпляра массива с помощью оператора new , необходимо прибегать к типу глобального обобщённого интерфейса Array

let animalData: string[] = new Array(); //Ok
let elephantData: string[] = new Array('Dambo'); // Ok

let lionData: (string | number)[];
lionData = new Array('Simba', 1); // Error
lionData = new Array('Simba'); // Ok
lionData = new Array(1); // Ok

let deerData: (string | number)[] = new Array<string | number>('Bambi', 1); // Ok

1.3 Tuple ([T0, T1, …, Tn]) тип кортеж

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

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

То есть мы строго ограничеваем КАЖДЫЙ элемент массива определенным типом.

let v0: [string, number] = ['Dambo', 1]; // Ok
let v1: [string, number] = [null, undefined]; // Error -> null не string, а undefined не number
let v3: [string, number] = [1, 'Simba']; // Error -> порядок обязателен
let v4: [string, number] = [,, ]; // Error -> пустые элементы массива приравниваются к undefined

Длина массива-значения должна соответствовать количеству типов, указанных в Tuple .

let elephantData: [string, number] = ['Dambo', 1]; // Ok
let lionData: [string, number] = ['Simba', 1, 1]; // Error, лишний элемент
let fawnData: [string, number] = ['Bambi']; // Error, не достает одного элемента
let giraffeData: [string, number] = []; // Error, не достает всех элементов

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

let elephantData: [string, number] = ['Dambo', 1];

elephantData.push(1941); // Ok
elephantData.push('Disney'); // Ok
elephantData.push(true); // Error, тип boolean, в, то время, как допустимы только типы совместимые с типами string и number
elephantData[10] = ''; // Ok
elephantData[11] = 0; // Ok
elephantData[0] = ''; // Ok, значение совместимо с типом заданном в кортеже
elephantData[0] = 0; // Error, значение не совместимо с типом заданном в кортеже

Дело в том, что элементы, чьи индексы выходят за пределы установленные кортежем, принадлежат к типу объединению ( Union ). Это означает, что элемент под индексом 2 принадлежит к типу string | number. Хотя тут тоже есть нюанс, напрямую через оператор присвоения назначить новый элемент нельзя, но можно добавить его через метод push. В последней строке возникает ошибка, потому что элемент под индексом 2 принадлежит к типу string | number , а это не, то же самое, что тип string . То есть он может быть либо строкой либо числом, а раз он может быть числом, то мы не можем его присвоить переменной которая может быть ТОЛЬКО строкой.

let elephantData: [string, number] = ['Dambo', 1]; // Ok

elephantData[2] = 'nuts'; // Type '"nuts"' is not assignable to type 'undefined'
elephantData.push('nuts') // Ok

let elephantName: string = elephantData[0]; // Ok, тип string
let elephantAge: number = elephantData[1]; // Ok, тип number
let elephantDiet: string = elephantData[2]; // Error, тип string | number

В случае, если описание кортежа может навредить семантике кода, его можно поместить в описание псевдонима типа ( type ).

type Tuple = [number, string, boolean, number, string];

let v1: [number, string, boolean, number, string]; // плохо
let v2: Tuple; // хорошо

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

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

function f(...rest: [number, string?, boolean?]): [number, string?, boolean?] {
  return rest
}

f(); // Error Expected 1-3 arguments, but got 0.
f(5); // Ok
f(5, ''); // Ok
f(5, '', true); // Ok

let l = f(5).length; // let l: 1 | 2 | 3

Кроме того, для кортежа применим механизм распространения ( spread ), который может быть указан в любой части определения типа. Но существуют два исключения. Во-первых, определение типа кортежа может включать только одно распространение. И во вторых, распространение не может быть указанно перед необязательными типами. В результате распространения, получается тип с логически предсказуемой последовательностью типов, определяющих кортеж.

let v0: [...boolean[], ...string[]]; // Error A rest element cannot follow another rest element.
let v1: [...boolean[], boolean, ...string[]]; // Error A rest element cannot follow another rest element.
let v2: [...boolean[], number]; // Ok
let v3: [number, ...boolean[]]; // Ok
let v4: [number, ...boolean[], number]; // Ok

let v5: [...boolean[], boolean?]; // Error An optional element cannot follow a rest element.
let v6: [boolean?, ...boolean[]]; // Ok

type Strings = [string, string];
type BooleanArray = boolean[];

type Unbounded0 = [...Strings, ...BooleanArray, symbol]; // type Unbounded0 = [string, string, ...boolean[], symbol]
type Unbounded1 = [ ...Strings, ...BooleanArray, symbol, ...Strings] // [string, string, ...boolean[], symbol, string, string]

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

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

const f = (p: [string, number]) => {}
/**
* автодополнение -> f(p: [string, number]): void
*
* Совершенно не понятно чем конкретно являются
* элементы представляемые типами string и number
*/
f0()
const f = (p: [a: string, b: number]) => {};
/**
* автодополнение -> f(p: [a: string, b: number]): void
*
* Теперь мы знаем, что функция ожидает не просто
* строку и число, а аргумент "a" и аргумент "b",
* которые в реальном проекте будут иметь более
* осмысленное смысловое значение, например "name" и "age".
*/
f1()

Тип переменной при присвоении ей инициализированного массива без явного указания типа, будет выведен как массив. Другими словами, вывод типа неспособен вывести тип кортеж.

2. Объектные типы с индексными членами (объектный тип с динамическими ключами)

2.1 Индексные члены (определение динамических ключей)

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

Индексная сигнатура (index signature) состоит из двух частей. В первой части расположен имеющий собственную аннотацию типа идентификатор привязки (binding identifier) заключенный в квадратные скобки [] . Во второй части расположена аннотация типа (type annotation) представляющего значение ассоциируемое с динамическим ключом.

Синтаксис:

{ [identifier: Type]: Type }

Идентификатору привязки можно дать любое имя, которое должно быть ассоциировано только с типами string , number , symbol или literal template string , а в качестве типа указанного справа от двоеточия, может быть указан любой тип.

// именование ключа - идентификатор ключа может быть любым
interface Identifire {
  [identifire: string]: string
}

interface Identifire_2 {
  [someKey: string]: string
}

// допустимые типы - string, number, symbol или litelral template string
interface Identifier_3 {
  [key: string]: string; // будет соответствовать o[`key`]
}

interface Identifier_4 {
  [key: number]: string; // будет соответствовать o[5]
}

interface Identifier_5 {
  [key: symbol]: string; // будет соответствовать o[Symbol(`key`)]
}

interface Identifier_6 {
  [key: `data-${string}`]: string; // будет соответствовать o[`data-*`]
}

В одном объектном типе одновременно могут быть объявлены индексные сигнатуры, чьи идентификаторы привязки принадлежат к типу string , number , symbol или literal template string . Но с одной оговоркой. Их типы, указанные в аннотации типов, должны быть совместимы

interface A {
  [key: string]: string;
  [key: number]: string;
  [key: symbol]: string;
  [key: `data-${string}`]: string;
}

let a: A = {
  validKeyDeclareStatic: 'value', // Ok, значение принадлежит к string
  invalidKeyDeclareStatic: 0, // Error, значение должно быть совместимым с типом string
};

a.validKeyDefineDynamicKey = 'value'; // Ok
a.invalidKeyDefineDynamicKey = 0; // Error, значение должно быть совместимым с типом string

a[0] = 'value'; // Ok


interface B {
  [identifier: string]: string; // Ok
  [identifier: string]: string; // Error, дубликат
}
interface С {
  [identifier: string]: string; // Ok
  [identifier: number]: number; // Error, должен принадлежать к типу string
}
class SuperClass { // суперкласс
  a: number;
}

class SubClass extends SuperClass { // подкласс
  b: number;
}

interface D {
  [identifier: string]: SuperClass; // Ok
  [identifier: number]: SubClass; // Ok, SubClass совместим с SuperClass
}

let d: D = {};
d.dynamicKey = new SubClass(); // Ok
d[0] = new SubClass(); // Ok

interface E {
  [identifier: string]: SubClass; // Ok
  [identifier: number]: SuperClass; // Error, SuperClass несовместим с SubClass
}

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

interface A {
  [key: string | number | symbol | `data-${string}`]: string; // это тоже самое, что и...
}

interface B { // ...это
  [key: string]: string;
  [key: number]: string;
  [key: symbol]: string;
  [key: `data-${string}`]: string;
}

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

class Identifier {
  static [key: string]: string; // статическая индексная сигнатура

  [key: string]: string;

  [key: number]: string;

  [0]: 'value';

  [1]: 5; // Error, все члены должны принадлежать к совместимым со string типам

  public a: string = 'value'; // Ok, поле name с типом string

  public b: number = 0; // Error, все члены должны принадлежать к совместимым со string типам

  public c(): void {} // Error, метод тоже член и на него распространяются те же правила
}

let identifier: Identifier = new Identifier();

/**индексная сигнатура экземпляра класса */
identifier.validDynamicKey = 'value'; // Ok
identifier.invalidDynamicKey = 0; // Error
identifier[2] = 'value'; // Ok
identifier[3] = 0; // Error

/**индексная сигнатура класса */
Identifier.validDynamicKey = 'value'; // Ok
Identifier.invalidDynamicKey = 0; // Error
Identifier[2] = 'value'; // Ok
Identifier[3] = 0; // Error

Кроме того, классы накладывают ограничение не позволяющее использовать модификаторы доступа ( private , protected , public ). При попытке указать данные модификаторы для индексной сигнатуры возникнет ошибка. Но, относительно модификаторов, есть несколько нюансов, связанных с модификатором readonly. При указании модификатора readonly искажается смысл использования индексной сигнатуры, так как это дает ровно противоположный эффект. Вместо объекта с возможностью определения динамических членов получается объект, позволяющий лишь объявлять динамические ключи, которым нельзя ничего присвоить. То есть, объект полностью закрыт для изменения.

interface IIdentifier {
  readonly [key: string]: string; // Ok, модификатор readonly
}

let instanceObject: IIdentifier = {};

instanceObject.a; // Ok, можно объявить
instanceObject.a = 'value'; // Error, но нельзя присвоить значение

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

interface IIdentifier {
  [key: string]: string;
  a: string; // Ok, [в момент декларации]
  b: number; // Error, [в момент декларации] допускается объявление идентификаторов принадлежащих только к типу string
}

let instanceObject: IIdentifier = {
  c: '', // Ok, [в момент объявления]
  d: 0 // Error, [в момент объявления] допускается объявление идентификаторов принадлежащих только типу string
};

instanceObject.e = ''; // Ok, [после объявления]
instanceObject.f = 0; // Error, [после объявления] допускается объявление идентификаторов принадлежащих только типу string

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

interface IIdentifier {
  readonly [key: string]: string; // Ok
  a: string; // Ok, декларация
}

let instanceObject: IIdentifier = {
  a: '', // Ok, объявление
  b: '' // Ok, объявление
};

instanceObject.с = 'value'; // Error, ассоциировать ключ со значением после создания объекта по-прежнему нельзя

К тому же объекты и классы имеющие определение индексной сигнатуры не могут содержать определения методов.

interface IIdentifier {
  readonly [key: string]: string;
  method(): void; // Error -> TS2411: Property 'method' of type '() => void' is not assignable to string index type 'string'
}

class Identifier {
  readonly [key: string]: string;
  method(): void {} // Error -> TS2411: Property 'method' of type '() => void' is not assignable to string index type 'string'.
}

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

interface IIdentifier {
  readonly [key: string]: string; // Ok
  a: string; // Ok, декларация
}

let instanceObject: IIdentifier = {
  a: 'value', // Ok, реализация
  b: 'value' // Ok, объявление
};

instanceObject.a = 'new value'; // Ok, можно перезаписать значение
instanceObject.b = 'new value'; // Error, нельзя перезаписать значение

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

Если идентификатор привязки принадлежит к типу string , то в качестве ключа может быть использовано значение, принадлежащее к типам string , number , symbol , Number Enum и String Enum.

interface StringDynamicKey {
  [key: string]: string;
}
enum NumberEnum { Prop = 0 }
enum StringEnum { Prop = 'prop' }

let example: StringDynamicKey = {
  property: '', // Ok String key
  'key': '', // Ok String key
  1: '', // Ok Number key
  [Symbol.for('key')]: '', // Ok Symbol key
  [NumberEnum.Prop]: '', // Ok Number Enum key
  [StringEnum.Prop]: '', // Ok String Enum key
};

В случае, когда идентификатор привязки принадлежит к типу number , то значение, используемое в качестве ключа, может принадлежать к таким типам, как number , symbol , Number Enum и String Enum .

interface NumberDynamicKey {
  [key: number]: string;
}

enum NumberEnum { Prop = 0 }
enum StringEnum { Prop = 'prop' }

let example: NumberDynamicKey = {
  property: '', // Error String key
  '': '', // Error String key
  1: '', // Ok Number key
  [Symbol.for('key')]: '', // Ok Symbol key
  [NumberEnum.Prop]: '', // Ok Number Enum key
  [StringEnum.Prop]: '', // Ok String Enum key
};

Вывод типов, в некоторых случаях, выводит тип, принадлежащий к объектному типу с индексной сигнатурой.

let computedIdentifier = 'e';

// let v: {
//    a: string;
//    b: string;
// }
let v = {
  a: '', // объявление идентификатора привычным способом

  ['b']: '', // объявление идентификатора с помощью строкового литерала.
}

// let y: {
//    [x: string]: string;
// }
let y = {
  ['c' + 'd']: '', // объявление идентификатора с помощью выражениясо строковыми литералами
}

// let x: {
//    [computedIdentifier]: string;
// }
let x = {
  [computedIdentifier]: '' // объявление идентификатора при помощи вычисляемого идентификатора  
}

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

2.2 Строгая проверка при обращении к динамическим ключам

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

type T = {
  [key: string]: number | string;
}

function f(p: T) {
  /**
  * Обращение к несуществующим полям
  */
  
  p.bad.toString(); // Ok -> Ошибка времени исполнения
  p[Math.random()].toString(); // Ok -> Ошибка времени исполнения
}

Данная проблема решается с помощью флага --noUncheckedIndexedAccess активирующего строгую проверку при обращении к динамическим членам объектных типов.

Активация механизма позволяет обращаться к динамическим членам только после подтверждения их наличия в объекте, а также совместно с такими операторами, как оператор опциональной последовательности ?. и опциональный оператор !. .

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

type T = {
  [key: string]: number | string;
}

function f (p: T) {
  // Проверка наличия поля bad
  if ('bad' in p) {
    p.bad?.toString() // Ok поле точно существует
  }
}

2.3 Операторы ?. и !.

Давайте отдельно рассмотрим принцип работы опциональных операторов.

По сути, опциональный оператор ? позволяет нам писать код, в котором TypeScript может немедленно остановить выполнение некоторых выражений при столкновении с оператором nullили undefined.

let x = foo?.bar.baz();

Это способ сказать, что когда foo определено, foo.bar.baz()будет вычислено; но когда foo равно null или undefined, останавливаем то, что мы делаем, и просто возвращаемся undefined».

Проще говоря, этот фрагмент кода эквивалентен следующему.

let x = foo === null || foo === undefined ? undefined : foo.bar.baz();

Обратите внимание, что если bar равно null или undefined, наш код всё равно столкнётся с ошибкой при доступе к baz. Аналогично, если baz равно null или undefined, мы столкнёмся с ошибкой в ​​месте вызова. ?.проверяет только, является ли значение слева от него каким-либо из последующих свойств nullили нет.undefined.

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

type T = {
  bar?: { baz?: () => void }
}

const foo: T = {}

const x = foo?.bar?.baz?.()

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

/**
 * Получить первый элемент массива, если мы имеем массив
 * В противном случае вернуть undefined.
 */
function tryGetFirstElement<T> (arr?: T[]) {
  return arr?.[0];

  // эквивалентно
  //   return (arr === null || arr === undefined) ?
  //       undefined :
  //       arr[0];
}

Также есть необязательный вызов, который позволяет нам условно вызывать выражения, если они не являются null или undefined.

async function makeRequest(url: string, log?: (msg: string) => void) {
  log?.(`Request started at ${new Date().toISOString()}`);
  // roughly equivalent to
  //   if (log != null) {
  //       log(`Request started at ${new Date().toISOString()}`);
  //   }
  const result = (await fetch(url)).json();
  log?.(`Request finished at ${new Date().toISOString()}`);
  return result;
}

«Короткое замыкание», характерное для опциональных цепочек, ограничивает доступ к свойствам, вызовам и элементам — оно не выходит за пределы этих выражений. Другими словами, данное выражение не предотвращает деление или someComputation()вызов:

let result = foo?.bar / someComputation();

Это может привести к делению undefined, поэтому в strictNullChecks, следующее является ошибкой.

function barPercentage(foo?: { bar: number }) {
  return foo?.bar / 100;
  //     ~~~~~~~~
  // Error: Object is possibly undefined.
}

Оператор ! (Non-Null and Non-Undefined Operator)

Оператор Not-Null Not-Undefined при активной опции --strictNullChecks в случаях, допускающих обращение к несуществующим членам, позволяет приглушать сообщения об ошибках.

!постфиксный оператор выражения может использоваться для подтверждения того, что его операнд не равен NULL и не является неопределённым в контекстах, где средство проверки типов не может определить этот факт.

Простыми словами, когда в режиме --strictNullChecks происходит обращение к значению объекта или метода, которые могут иметь значение null или undefined, компилятор с целью предотвращения возможной ошибки накладывает запрет на операции обращения и вызова. Разрешить подобные операции возможно с помощью оператора Not-Null Not-Undefined, который обозначается восклицательным знаком !.

Для примера рассмотрим:

/** strictNullChecks: true */

type UserEvent = { type: string };

// параметр помечен как необязательный,
// поэтому тип выводится как event?: UserEvent | undefined
function handler(event?: UserEvent): void {
  // потенциальная ошибка, возможно обращение к полю несуществующего объекта
  let type = event.type; // Error -> возможная ошибка во время выполнения
}

Обычно в таких случаях стоит изменить архитектуру, но если разработчик в полной мере осознает последствия, то компилятор можно настоятельно попросить закрыть глаза на потенциально опасное место при помощи оператора Not-Null Not-Undefined. При обращении к полям и свойствам объекта, оператор Not-Null Not-Undefined указывается перед оператором точка object!.field.

/** strictNullChecks: true  */

type UserEvent = { type: string };

function handler(event?: UserEvent): void {
  // указываем компилятору, что берем этот
  // участок кода под собственный контроль
  let type = event!.type; // Ok
}

Оператор Not-Null Not-Undefined нужно повторять каждый раз, когда происходит обращение к полям и свойствам объекта, помеченного как необязательный.

/** strictNullChecks: true  */

type Target = { name: string };

type Currenttarget = { name: string };

type UserEvent = {
  type: string;
  target?: Target;
  currentTarget: Currenttarget;
};

function handler(event?: UserEvent): void {
  let type = event!.type; // 1 !
  let target = event!.target!.name; // 2 !
  let currenttarget = event!.currentTarget.name; // 1 !
}

При обращении к необязательным методам объекта, оператор Not-Null Not-Undefined указывается между идентификатором (именем) и круглыми скобками. Стоит обратить внимание, что когда происходит обращение к необязательному полю или свойству объекта, оператор Not-Null Not-Undefined указывается лишь один раз optioanlObject!.firstLevel.secondLevel. При обращении к необязательному методу того же объекта, оператор Not-Null Not-Undefined указывается дважды optionalObject!.toString!().

/** strictNullChecks: true  */

type Target = { name: string };

type Currenttarget = { name: string };

type UserEvent = {
  type: string;
  target?: Target;
  currentTarget: Currenttarget;
  toString?(): string;
};

function handler(event?: UserEvent): void {
  let type = event!.type; // 1 !
  let target = event!.target!.name; // 2 !
  let currenttarget = event!.currentTarget.name; // 1 !
  let meta = event!.toString!(); // 2 !
}

3. Оператор keyof, Lookup Types, Mapped Types, Mapped Types - префиксы + и -

3.1 Запрос ключей keyof

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

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

type Person = {
  name: string;
  age: number;
  email: string;
};

type PersonKeys = keyof Person;  // "name" | "age" | "email"

Оператор keyof может применяться к любому типу данных.

type AliasType = { f1: number, f2: string };

interface IInterfaceType {
  f1: number;
  f2: string;
}

class ClassType {
  f1: number;
  f2: string;
}
let v1: keyof AliasType; // v1: "f1" | "f2"
let v2: keyof IInterfaceType; // v2: "f1" | "f2"
let v3: keyof ClassType; // v3: "f1" | "f2"
let v4: keyof number; // v4: "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"

Оператор keyof выводит только публичные не статические ключи типа:

class Type {
  public constructor() {}

  public static fieldClass: number;
  public static methodClass(): void {}

  private privateField: number;
  protected protectedField: string;

  public publicField: boolean;
  public get property(): number { return NaN; }
  public set property(value: number) {}
  public instanceMethod(): void {}
}

let v1: keyof Type; // a: "publicField" | "property" | "instanceMethod"

В случае, если тип данных не содержит публичных ключей, оператор keyof выведет тип never

type AliasType = {};

interface IInterfaceType {}

class ClassType {
  private f1: number;
  protected f2: string;
}

let v1: keyof AliasType; // v1: never
let v2: keyof IInterfaceType; // v2: never
let v3: keyof ClassType; // v3: never
let v4: keyof object; // v4: never

Напоследок стоит упомянуть об одном не очевидном моменте: оператор keyof можно совмещать с оператором typeof (Type Queries).

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

const person = {
  name: 'John',
  age: 30,
  email: 'john@example.com',
}

type PersonType = typeof person; // PersonType: { name: string, age: number, email: string }

type PersonKey = keyof typeof person // "name" | "age" | "email"

3.2 Поиск типов (Lookup Types)

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

interface Person {
  name: string
  age: number
}

type PersonNameType = Person['name'] // : string

type PersonAgeType = Person['age'] // : number

type PersonPhoneType = Person['phone'] // Error: Property 'phone' does not exist on type 'Person'

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

class ClassType {
  public static publicFieldClass: number;

  public publicInstanceField: number;

  protected protectedInstanceField: string;
  private privateInstanceField: boolean;

  public get propertyInstance(): number { return NaN; }
  public set propertyInstance(value: number) {}

  public methodInstance(): void {}
}

let publicFieldClass: ClassType['publicFieldClass']; // Error

let publicFieldInstance: ClassType['publicInstanceField']; // publicFieldInstance: number

let protectedFieldInstance: ClassType['protectedInstanceField']; //protectedFieldInstance: string

let privateFieldInstance: ClassType['privateInstanceField']; // privateFieldInstance: boolean

let propertyInstance: ClassType['propertyInstance']; // propertyInstance: number

let methodInstance: ClassType['methodInstance']; // methodInstance: () => void

let notExist: ClassType['notExist']; // Error

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

К примеру следующий тип CheckString проверяет, являются ли указанные поля типа string и если да, то он вернет их тип иначе он вернет never.

type CheckString<T, K extends keyof T> = T[K] extends string ? T[K] : never

type Person = {
  name: string,
  sity: string,
  age: number
}

declare const person_1: CheckString<Person, 'name' | 'sity'> // :string

declare const person_2: CheckString<Person, 'age' | 'sity'> // :never

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

type Tuple = [string, number]

declare const tuple: Tuple[0] // tuple: string

declare const tuple_all: Tuple[number] // tuple_all: string | number

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

type ArrType = string[]

declare const arr: ArrType[0] // arr: string

declare const arr_all: ArrType[number] // arr: string

3.3 Сопоставление типов (Mapped Types)

Сопоставленные типы — это типы данных, которые при помощи механизма итерации модифицируют лежащие в основе конкретные типы данных.

В TypeScript существует возможность определения типа, источником ключей которого выступает множество определяемое литеральными строковыми типами. Подобные типы обозначаются как сопоставленные типы Mapped Types и определяются исключительно на основе псевдонимов типов Type Alias, объявление которых осуществляется при помощи ключевого слова type . Тело сопоставимого типа, заключенное в фигурные скобки {} , включает в себя одно единственное выражение, состоящие из двух частей разделенных двоеточием.

Рассмотрим синтаксис

type Сопоставимый_тип = {
  Левая_часть_выражения: Правая_часть_выражения;
}

В левой части выражения располагается обрамленное в квадратные скобки [] выражение, предназначенное для работы с множеством, а в правой части определяется произвольный тип данных.

type Сопоставимый_тип = {
  [Выражение_для_работы_с_множеством]: Произвольный_тип_данных;
}

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

type Сопоставимый_тип = {
  [Произвольный_идентификатор in Множество]: Произвольный_тип_данных;
}

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

type Сопоставимый_тип = {
  [Key in Множество]: Произвольный_тип_данных;
}
// или
type Сопоставимый_тип = {
  [K in Множество]: Произвольный_тип_данных;
}

Множество может быть определенно как единственным литеральным строковым типом ( ElementLiteralStringType ), так и его множеством, составляющим тип объединение ( Union Type ) ( FirstElementLiteralStringType | SecondElementLiteralStringType ).

// множество с одним элементом
type Сопоставимый_тип = {
  [Key in "First"]: Произвольный_тип_данных;
}

// или

// множество с несколькими элементами
type Сопоставимый_тип = {
  [Key in "First" | "Second"] : Произвольный_тип_данных;
}

// или

type LiteralStringType = "First" | "Second";

// множество с несколькими элементами вынесенных в тип Union
type Сопоставимый_тип = {
  [Key in LiteralStringType]: Произвольный_тип_данных;
}

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

К примеру , у нас есть обьектный тип с членами 'a' 'b' 'c' которые имеют единый тип данных string мы бы могли записать его так:

type SomeType = {
  a: string,
  b: string,
  c: string,
}

const obj: SomeType = {
  a: 'value_1',
  b: 'value_2',
  c: 'value_3',
}

Либо мы бы могли записать его в виде выражения которое мы рассмотрели выше [ПроизвольныйИдентификатор in Множество]:

type SomeType = {
  [key in 'a' | 'b' | 'c']: string
}

const obj: SomeType = {
  a: 'value_1',
  b: 'value_2',
  c: 'value_3',
}

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

Пример с Enum

enum KEYS {
  Key_1 = 'a',
  Key_2 = 'b',
  Key_3 = 'c'
}

type SomeType = {
  [key in KEYS]: string
}

const obj: SomeType = {
  a: 'value_1',
  b: 'value_2',
  c: 'value_3',
}

Пример с Tuple

type KEYS = ['a', 'b', 'c']

type SomeType = {
  [key in KEYS[number]]: string
}

const obj: SomeType = {
  a: 'value_1',
  b: 'value_2',
  c: 'value_3',
}

Пример с извлечением ключей из класса с помощью оператора keyof

class SomeClass {
  public a: string

  public b: string

  public c: string
}

type SomeType = {
  [key in keyof SomeClass]: string
}

const obj: SomeType = {
  a: 'value_1',
  b: 'value_2',
  c: 'value_3',
}

От статического указания итерируемого типа мало пользы, поэтому Mapped Types лучше всего раскрывают свой потенциал при совместной работе с известными к этому моменту запросом ключей ( keyof ) и поиском типов ( Lookup Types ), оперирующих параметрами типа ( Generics ).

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

В выражении [P in keyof T]: T[P]; первым действием выполняется вычисление оператора keyof над параметром типа T . В его результате ключи произвольного типа преобразуются во множество, то есть в тип Union , элементы которого принадлежат к литеральному строковому типу данных. Простыми словами операция keyof T заменяется на только, что полученный тип Union [P in Union]: T[P]; , над которым на следующим действии выполняется итерация. С полученным в итерации [P in Union] ключом K ассоциируется тип ассоциированный с ним в исходном типе и полученный с помощью механизма поиска типов T[K].

Совокупность описанных механизмов позволяет определять не только новый тип, но и создавать модифицирующие типы, которые будут способны добавлять модификаторы, как например readonly или ?.

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

interface IPerson {
  name: string,
  age: number,
}

type Optional<T> = {
  [K in keyof T]?: T[K]
}

const person: Optional<IPerson> = { name: 'str' }

Пример 2, здесь из произвольного типа выбираются только те ключи которые были переданы вторым параметром. Таким образом из Person мы извлекли только ключ 'age', а не полностью скопировали весь тип.

interface IPerson {
  name: string,
  age: number,
}

type MyPick<T, U extends keyof T> = {
  [K in U]: T[K]
}

const person: MyPick<IPerson, 'age'> = { age: 10 }

Во втором случае MappedType оператор keyof также преобразует параметр типа T в тип Union , который затем расширяет параметр типа U , тем самым получая все его признаки, необходимые для итерации в выражении [K in U]. С полученным в итерации [K in U] ключом K ассоциируется тип ассоциированный с ним в исходном типе и полученный с помощью механизма поиска типов T[K] .

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

type Nullable<T> = {
  [P in keyof T]: T[P] | null;
}

type Stringify<T> = {
  [P in keyof T]: string;
}

interface IAnimal {
  name: string;
  age: number;
}

let nullable: Nullable<IAnimal>; // { name: string | null; age: number | null; }

let stringify: Stringify<IAnimal>; // { name: string; age: string; }

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

type AliasType<T, U> = {
  [P in keyof T]: T[P]; // Ok
  [V in keyof U]: U[V]; // Error
  f1: number; // Error
}

К тому же в TypeScript существует несколько готовых типов, таких как Readonly , Partial , Record и Pick.

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

type T = {
  [K in STRING_VALUES as NEW_KEY]: K // K преобразованный
}

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

Следующий пример возвращает тип только с членами у которых идентификатор представлен числом

type OnlyNumber<T> = {
  [K in keyof T as K extends number ? K : never]: T[K]
}

interface IData {
  a: string
  10: number
  20: number
  b: boolean
}

type MyType = OnlyNumber<IData> // { 10: number; 20: number; }

Данный пример добавляет к названию идентификаторов приставку get_ и конвертирует их в соответствующие методы: name: string -> get_name: () => string

type ToGetter<T extends PropertyKey> = T extends string ? `get_${T}` : never;

type Getters<T> = {
  [K in keyof T as ToGetter<K>]: () => T[K];
}

type Person = {
  name: string;
  age: number;
}


/**
* type T = {
*   get_name: () => string;
*   get_age: () => number;
* }
*/
type T = Getters<Person>

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

type MyOmit<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never: P]: T[P]
}

type Person = {
  name: string;
  age: number;
  gender: string;
}

type MyPerson = MyOmit<Person, 'name' | 'age'> // { gender: string; }

3.4 Префиксы + и - в сопоставленных типах

Сопоставленные типы позволяют добавлять модификаторы, но не позволяют их удалять, Для разрешения данной проблемы, к модификаторам в типах сопоставления, были добавлены префиксы + и - , с помощью которых реализуется поведение модификатора — добавить ( + ) или удалить ( - ).

Следующий пример делает все члены типа не обязательными

type AddModifier<T> = {
  [K in keyof T]?: T[K]
}

interface IPerson {
  name: string
  age: number
}

/**
 * let person: {
 *   name?: string;
 *   age?: number;
 * }
 */
let person: AddModifier<IPerson> = {}
type AddModifier<T> = {
  +readonly [P in keyof T]+?: T[P]; // добавит модификаторы readonly и ? (optional)
};

type RemoveModifier<T> = {
  -readonly [P in keyof T]-?: T[P]; // удалит модификаторы readonly и ? (optional)
};

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

4.1 Условные типы на практике

Условные типы (Conditional Types) — это типы, способные принимать одно из двух значений, основываясь на принадлежности одного типу к другому. Условные типы семантически схожи с тернарным оператором.

T extends U ? T1 : T2

В блоке выражения с помощью ключевого слова extends устанавливается принадлежность к заданному типу. Если тип, указанный слева от ключевого слова extends , совместим с типом, указанным по правую сторону, то условный тип будет принадлежать к типу T1 , иначе — к типу T2 . Стоит заметить, что в качестве типов T1 и T2 могут выступать, в том числе и другие условные типы, что в свою очередь создаст цепочку условий определения типа.

Данный тип просто проверяет является ли переданный параметр типа строкой и возврашает true или false соответственно

type isString<T> = T extends string ? true : false

type A = isString<'test'> // type A = true

type B = isString<10> // type B = false

Помимо конкретного типа, в качестве правого (от ключевого слова extends ) операнда
также может выступать другой параметр типа. Этот пример проверяет совместим ли параметр типа T с параметром типа U и возвращает true или false соответственно

type Check<T, U> = T extends U ? true : false

type A = Check<'test', string> // type A = true

type B = Check<10, string> // type B = false

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

type Check<T, U> = T extends U ? true : never

type A = Check<10 | 'test', string> // type A = true

4.2 Распределительные условные типы (Distributive Conditional Types)

Условные типы, которым в качестве аргумента типа устанавливается объединенный тип Union Type называются распределительные условные типы ( Distributive Conditional Types ). Называются они так, потому, что каждый тип, составляющий объединенный тип, будет распределен таким образом, что бы выражение условного типа было выполнено для каждого. Это, в свою очередь может определить условный тип, как тип объединение.

type Check<T> =
  T extends number
    ? 'number'
    : T extends string
      ? 'string'
    : never

type A = Check<'test' | 10> // type A = "string" | "number"

type B = Check<true | object> // type B = never

Для понимания, почему на выходе получился Union тип давайте рассмотрим работу компилятора на примере вывода типа дляtype A из кода выше:

  1. получаем первый тип, составляющий union тип (в данном случае 'test') и начинаем подставлять его на место T

  2. 'test' соответствует number? Нет! Продолжаем...

  3. 'test' соответствует string? Да! Определяем 'string'.

  4. закончили определять один тип, приступаем к другому

  5. 10 соответствует number? Да! Определяем 'number'

  6. Итого: условный тип type A = Check<'test' | 10> определен, как "string" | "number"

4.3 Вывод типов в условном типе (ключевое слово infer)

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

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

function f(param: string): void {}

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

type FunctionParamType <F> = F extends (p: infer U) => void ? U : never

Таким образом мы можем извлечь и вывести тип аргумента функции:

function fString (param: string): void {}
function fNumber (param: number): void {}


type FunctionParamType <F> = F extends (p: infer U) => void ? U : never


type f1 = FunctionParamType<typeof fString> // type f1 = string
type f2 = FunctionParamType<typeof fNumber> // type f1 = number

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

Следующий пример выводит тип последнего элемента картежа (Tuple ):

type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]

type Last <A> = A extends [...n, infer T] ? T : never

type tail1 = Last<arr1> // 'c'
type tail2 = Last<arr2> // 1

Данный пример выводит тип для члена a обьектного типа:

interface A { a: string }
interface B { a: number }
interface C {}

type getA<T> = T extends { a: infer U } ? U : never

type a = getA<A> // string
type b = getA<B> // number
type c = getA<C> // never

Данный пример превращает тип кортежа (Tuple) в обьединение (Union):

type Arr = ['1', '2', '3']

type TupleToUnion<T> = T extends Array<infer ITEMS> ? ITEMS : never

type Test = TupleToUnion<Arr> // expected to be '1' | '2' | '3'

Данный пример удаляет пробелы в начале строки:

type Space = ' ' | '\n' | '\t'
type TrimLeft<S extends string> = S extends `${Space}${infer R}` ? TrimLeft<R> : S

type trimmed = TrimLeft<'  Hello World  '> // type trimmed = "Hello World  "

Следующий пример возвращает строку, но первая буква заглавная

type Capitalize<S extends string> = S extends `${infer X}${infer U}` ? `${Uppercase<X>}${U}` : S;

type capitalized = Capitalize<'hello world'> // type capitalized = "Hello world"

Следующие 2 примера выводит длинну строки:

type LengthOfString<S extends string, T extends any[] = []> =
  S extends `${infer _}${infer B}`
    ? LengthOfString<B, [...T, any]>
    : T["length"];

type S = LengthOfString<'hello'> // type S = 5
type StringToArray<S extends string> = S extends `${infer T}${infer R}` ? [T, ...StringToArray<R>] : [];
type LengthOfString<S extends string> = StringToArray<S>['length']

type S = LengthOfString<'hello'> // type S = 5

Следующий пример принимает строку и выводит тип обьединения (Union) для входных символов:

type StringToUnion<T extends string> = T extends `${infer Let}${infer Rest}`
  ? Let | StringToUnion<Rest>
  : never

type test = StringToUnion<'qwerty'> // type test = "q" | "w" | "e" | "r" | "t" | "y"

Следуюший пример вырезает первый элемент кортежа (Tuple)

type Shift<T> = T extends [any, ...infer U] ? U : never

type Result = Shift<[3, 2, 1]> // [2, 1]

Следуюший пример возвращает элементы кортежа (Tuple) в обратном порядке

type Reverse<T extends any[]> = T extends [infer A, ...infer Rest] ? [...Reverse<Rest>, A] : T

type a = Reverse<['a', 'b']> // ['b', 'a']
type b = Reverse<['a', 'b', 'c']> // ['c', 'b', 'a']

Существует механизм позволяющий ограничивать переменные типа infer конкретным типом при помощи ключевого слова extends. Давайте рассмотрим его работу.

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

type FirstNumberItem<T> =
  T extends [infer S, ...unknown[]]
    ? S extends number ? S : never
    : never;

type A = FirstNumberItem<[number, boolean, string]>;  // type A = number

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

type FirstNumberItem<T> =
  T extends [infer S extends number, ...unknown[]] ? S : never;

5. Встроенные типы

Чтобы сделать повседневные будни разработчика немного легче, TypeScript, реализовал
несколько предопределенных сопоставимых типов.

Вот список некотрых предустановленных типов которые вы можите использовать:

  • Partial<T> - Сделайте все свойства в T необязательными

  • Required<T> - Сделать все свойства в T обязательными

  • Readonly<T> - Сделать все свойства в T доступными только для чтения

  • Pick<T, K extends keyof T> - Из T выбираем набор свой��тв, ключи которых находятся в объединении K

  • Record<K extends keyof any, T> - Построить тип с набором свойств K типа T

  • Exclude<T, U> - Исключить из T те типы, которые можно отнести к U

  • Extract<T, U> - Извлечь из T те типы, которые можно присвоить U

  • Omit<T, K extends keyof any> - Построить тип со свойствами T, за исключением свойств типа K.

  • NonNullable<T> - Исключить null и undefined из T

  • Parameters<T extends (...args: any) => any> - Получить параметры типа функции в кортеже

  • ConstructorParameters<T extends abstract new (...args: any) => any> - Получить параметры типа функции-конструктора в кортеже

  • ReturnType<T extends (...args: any) => any> - Получить возвращаемый тип функции

  • InstanceType<T extends abstract new (...args: any) => any> - Получить возвращаемый тип функции-конструктора

  • Uppercase<S extends string> - Преобразовать строковый литерал в верхний регистр

  • Lowercase<S extends string> - Преобразовать строковый литерал в строчные буквы

  • Capitalize<S extends string> - Преобразовать первый символ строкового литерала в верхний регистр

  • Uncapitalize<S extends string> - Преобразовать первый символ строкового литерала в нижний регистр

  • NoInfer<T> - Маркер для позиции типа невывода

  • ArrayLike<T> - Составной тип для масивоподобных типов

  • Awaited<T> - Рекурсивно извлекает «ожидаемый тип» типа.

  • ThisParameterType<Type> - Извлекает тип параметра this для типа функции

  • OmitThisParameter<Type> - Удаляет thisпараметр из Type

  • ThisType<Type> - маркер контекстного thisтипа.

Далее рассмотрим их подробнее.

Readonly<T> - (сделать члены объекта только для чтения)

Сопоставимый тип Readonly добавляет каждому члену объекта модификатор readonly , делая их тем самым только для чтения.

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

interface IPerson {
  name: string;
  age: number;
}
/**
 * Функция, параметр которой не
 * защищен от случайного изменения.
 *
 * Поскольку объектные типы передаются
 * по ссылке, то с высокой долей вероятности,
 * случайное изменение поля name нарушит ожидаемый
 * ход выполнения программы.
 */
function mutableAction(person: IPerson) {
  person.name = "NewName"; // Ok
}

/**
* Надежная функция защищающая свои
* параметры от изменения не требуя описания
* нового неизменяемого типа.
*/
function immutableAction(person: Readonly<IPerson>) {
  person.name = "NewName"; // Error -> Cannot assign to 'name' because it is a read-only property.
}

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

interface IPerson {
  gender?: string;
}

type Person = Readonly<IPerson> // type Person = { readonly gender?: string; }

Partial<T> (сделать все члены объекта необязательными)

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

interface IPerson {
  readonly name: string;
}

/** type Person = {
 *    readonly name?: string;
 * }
 */
type Person = Partial<IPerson>

Required<T> (сделать все необязательные члены обязательными)

Сопоставимый тип Required удаляет все необязательные модификаторы ?: приводя члены объекта к обязательным.

interface IPerson {
  readonly name?: string;
}

/** type Person = {
 *    readonly name: string;
 * }
 */
type Person = Partial<IPerson>

Pick<T, K> - (отфильтровать объектный тип)

Сопоставимый тип Pick предназначен для фильтрации объектного типа ожидаемого в качестве первого параметра типа. Фильтрация происходит на основе ключей представленных множеством литеральных строковых типов ожидаемых в качестве второго параметра типа.

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

/* eslint-disable */

interface IPerson {
  name: string
  age: number
}

/**\
 * type Person = {
 *   name: string;
 * }
 */
type Person = Pick<IPerson, 'name'>

Record<K, T> (динамически определить поле в объектном типе)

Сопоставимый тип Record предназначен для динамического определения полей в объектном типе. Данный тип определяет два параметра типа. В качестве первого параметра ожидается множество ключей представленных множеством string или Literal String качестве второго параметра ожидается конкретный тип данных, который будет ассоциирован с каждым ключом.

По сути тип Record<K, T> это просто более лаконичный вариант замены индексных сигнатур.

/**
 * Поле payload определенно как объект
 * с индексной сигнатурой, что позволит
 * динамически записывать в него поля.
 */
interface IConfigurationIndexSignature {
  payload: {
    [key: string]: string
  }
}

/**
 * Поле payload определенно как
 * Record<string, string>, что аналогично
 * предыдущему варианту, но выглядит более
 * декларативно.
 */
interface IConfigurationWithRecord {
  payload: Record<string, string>
}

Но в отличии от индексной сигнатуры типа Record может ограничить диапазон ключей.

type WwwConfig = Record<'port' | 'domain', string>

const wwwConfig: WwwConfig = {
  port: '80',
  domain: 'https://domain.com',
}

Пример с Enum:

enum Directions {
  UP = 'up',
  DOWN = 'down',
}

/**
 * type Position = {
 *   up: string;
 *   down: string;
 * }
 */
type Position = Record<Directions, string>

const pos: Position = {
  up: '10px',
  down: '20px',
}

Пример с кортежем:

type Directions = ['up', 'down']

/**
 * type Position = {
 *   up: string;
 *   down: string;
 * }
 */
type Position = Record<Directions[number], string>

const pos: Position = {
  up: '10px',
  down: '20px',
}

Exclude<UnionType, ExcludedMembers> (исключает из T признаки присущие U)

В результате разрешения условный тип Exclude<T, U> будет представлять разницу типа T относительно типа U . Параметры типа T и U могут быть представлены как единичным типом, так и множеством union . Простыми словами из типа T будут исключены признаки (ключи) присущие также и типу U . В случае, если оба аргумента типа принадлежат к одному и тому же типу данных, Exclude будет представлен типом never .

// type T0 = "b" | "c"
type T0 = Exclude<"a" | "b" | "c", "a">;

// type T1 = string
type T1 = Exclude<number | string, number | boolean>

// type T2 = { b: number }
type T2 = Exclude<{ a: string } | { b: number }, { a: string }>

Extract<Type, Union> (общие для двух типов признаки)

В результате разрешения условный тип Extract будет представлять пересечение типа T относительно типа U . Оба параметра типа могут быть представлены как обычным типом, так union . Простыми словами, после разрешения Extract будет принадлежать к типу определяемого признаками (ключами) присущих обоим типам. В случае, когда общие признаки отсутствуют, тип Extract будет представлять тип never .

// type T0 = "a"
type T0 = Extract<"a" | "b" | "c", "a">;

// type T1 = number
type T1 = Extract<number | string, number | boolean>

// type T2 = { a: string }
type T2 = Extract<{ a: string } | { b: number }, { a: string }>

NonNullable<Type> (удаляет типы null и undefined)

Условный тип NonNullable служит для исключения из типа признаков типов null и undefined . Единственный параметр типа может принадлежать как к обычному типу, так и множеству определяемого тип union . В случае, когда тип, выступающий в роли единственного аргумента типа, принадлежит только к типам null и\или undefined , NonNullable представляет тип never .

// type T0 = string | number
type T0 = NonNullable<string | number | undefined>;

// type T1 = string[]
type T1 = NonNullable<string[] | null | undefined>;

// type T2 = string | number
type T2 = NonNullable<[string, undefined, number][number]>

ReturnType<T> (получить тип значения возвращаемого функцией)

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

// type T0 = string
type T0 = ReturnType<() => string>

// type T1 = void
type T1 = ReturnType<(s: string) => void>

// type T2 = unknown
type T2 = ReturnType<<T>() => T>

// type T3 = number[]
type T3 = ReturnType<<T extends U, U extends number[]>() => T>

// type T5 = any
type T5 = ReturnType<any>

declare function f1(): { a: number; b: string }

type T4 = ReturnType<typeof f1>
/**
 * type T4 = {
 *    a: number;
 *    b: string;
 * }
 */

InstanceType<T> (получить через тип класса тип его экземпляра)

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

class C {
  x = 0;
  y = 0;
}

/**
 * T0 = {
 *    x: number;
 *    y: number;
 * }
 */
type T0 = InstanceType<typeof C>

Parameters<T> (получить тип размеченного кортежа описывающий параметры функционального типа)

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

type Func = (a: string, b: number) => void

type Param = Parameters<Func> // type Param = [a: string, b: number]

ConstructorParameters<T> (получить через тип класса размеченный кортеж описывающий параметры его конструктора)

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

class Class {
  constructor(a: string, b: number) {}
}

// type Params = [a: string, b: number]
type Params = ConstructorParameters<typeof Class>

Omit<Type, Keys> (исключить из T признаки ассоциированными с ключами перечисленных множеством K)

Расширенный тип Omit предназначен для определения нового типа путем исключения заданных признаков из существующего типа.

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

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

type Person = {
  firstName: string;
  lastName: string;
  age: number;
};

/**
* Тип PersonName представляет только часть типа Person
*
* type PersonName = {
*   firstName: string;
*   lastName: string;
* }
*/
type PersonName = Omit<Person, 'age'>; // исключение признака с полем age из типа Person

Awaited<Type> (рекурсивное развертывания промисов)

Расширенный тип Awaited предназначен для рекурсивного развертывания промисов, что в повседневной работе с асинхронными операциями является незаменимым помощником.

Этот тип предназначен для моделирования операций, подобных await функциям  async или  .then() методам в Promises, а именно, способа, которым они рекурсивно разворачивают Promises.

// A = string
type A = Awaited<Promise<string>>;
// B = string
type B = Awaited<Promise<Promise<string>>>;
// C = string | number
type C = Awaited<string | Promise<number>>;

Реальный пример:

async function getUser(): Promise<{ id: number;  name: string }> {
    return { id: 1, name: 'John Doe' };
}

type User = Awaited<ReturnType<typeof getUser>>;

async function printUser(): Promise<void> {
  /**
   * const user: {
   *   id: number;
   *   name: string;
   * }
   */
  const user: User = await getUser();

  console.log(user);
}

printUser();
// {
//   "id": 1,
//   "name": "John Doe"
// }

Следующий пример с запросом

async function getUsers(): Promise<Array<{ name: string }>> {
  const response = await fetch("https://...com/users");
  
  return response.json();
}

type User = Awaited<ReturnType<typeof getUsers>>[0];

async function getUserNames() {
  const users = await getUsers();
  
  const names: Array<string> = await Promise.all(
    users.map(async (user: User) => {
      const name = user.name;
      
      return name;
    })
  );
  
  // const names: string[]
  console.log(names);
}

getUserNames();

NoInfer<Type> - Блокирует вывод данных к содержащемуся типу. За исключением блокировки выводов, NoInfer<Type>идентично Type.

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

function doSomething<T>(arg: T) {
    // ...
}

// Мы можем явно сказать, что «T» должно быть «string».
doSomething<string>("hello!");
// Мы также можем просто позволить вывести тип «Т».
doSomething("hello!");

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

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

function createStreetLight<C extends string>(colors: C[], defaultColor?: C) {
    // ...
}

createStreetLight(["red", "yellow", "green"], "red");

Что произойдёт, если мы передадим значение defaultColor, которого не было в исходном colorsмассиве? В этой функции , colorsкак предполагается, является «источником истины» и описывает, что можно передать вdefaultColor.

// Упс! Это нежелательно, но разрешено!
createStreetLight(["red", "yellow", "green"], "blue");

В этом вызове вывод типа решил, что "blue"тип так же допустим, как и "red""yellow"или "green". Поэтому вместо того, чтобы отклонить вызов, TypeScript выводит тип Cкак "red" | "yellow" | "green" | "blue". Это явно не то, что мы ожидали!

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

function createStreetLight<C extends string, D extends C>(colors: C[], defaultColor?: D) {
}
createStreetLight(["red", "yellow", "green"], "blue");
//                                            ~~~~~~
// error!
// Argument of type '"blue"' is not assignable to parameter of type '"red" | "yellow" | "green" | undefined'.

Это работает, но немного неудобно, поскольку D, вероятно, больше нигде не будет использоваться в сигнатуре для чего либо ещё. Хотя в данном случае createStreetLight это неплохо, использование параметра типа только один раз в сигнатуре часто является признаком некорректного кода.

Именно поэтому в TypeScript 5.4 появился новый NoInfer<T>служебный тип. Заключение типа в NoInfer<...>даёт TypeScript сигнал не копаться в нём и не искать кандидатов для вывода типов.

Используя NoInfer, мы можем переписать createStreetLightэто как-то так:

function createStreetLight<C extends string>(colors: C[], defaultColor?: NoInfer<C>) {
    // ...
}
createStreetLight(["red", "yellow", "green"], "blue");
//                                            ~~~~~~
// error!
// Argument of type '"blue"' is not assignable to parameter of type '"red" | "yellow" | "green" | undefined'.

Исключение типа defaultColorиз кандидата для вывода означает, что "blue"никогда не станет кандидатом на вывод, и средство проверки типов может его отклонить.

ThisParameterType<Type> - Извлекает тип параметра this для типа функции или unknown , если тип функции не имеет thisпараметра.

function toHex(this: Number) {
  return this.toString(16);
}
 
// (parameter) n: Number
function numberToString(n: ThisParameterType<typeof toHex>) {
  return toHex.apply(n);
}

OmitThisParameter<Type> - Удаляет thisпараметр из Type

Удаляет thisпараметр из Type. Если Typeявно объявленный параметр отсутствует this, результат — просто Type. В противном случае из создаётся Typeновый тип функции без параметра . Обобщения удаляются, и в новый тип функции распространяется только последняя сигнатура перегрузки.thisType

function toHex(this: Number) {
  return this.toString(16);
}

// const fiveToHex: () => string
const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5);

ThisType<Type> - Эта утилита не возвращает преобразованный тип. Вместо этого она служит маркером контекстного thisтипа.

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

type ObjectDescriptor<D, M> = {
  data?: D;
  methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M
};
 
function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
  let data: object = desc.data || {};
  let methods: object = desc.methods || {};
  return { ...data, ...methods } as D & M;
}
 
let obj = makeObject({
  data: { x: 0, y: 0 },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx; // Strongly typed this
      this.y += dy; // Strongly typed this
    },
  },
});
 
obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);

В приведенном выше примере methodsобъект в аргументе to makeObjectимеет контекстный тип, который включает , ThisType<D & M>и поэтому тип this в методах внутри methodsобъекта — { x: number, y: number } & { moveBy(dx: number, dy: number): void }. Обратите внимание, что тип свойства methodsодновременно является целью вывода и источником для thisтипа в методах.

Интерфейс ThisType<T>маркера — это просто пустой интерфейс, объявленный в lib.d.ts. Помимо распознавания в контекстном типе литерала объекта, этот интерфейс действует как любой пустой интерфейс.

Массивоподобные readonly типы ReadonlyArray<T> , ReadonlyMap<T> , ReadonlySet<T>

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

let array: readonly string[] = ['Kent', 'Clark']; // Массив

let tuple: readonly [string, string] = ['Kent', 'Clark']; // Кортеж

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

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

let array: readonly string[] = ['Kent', 'Clark']; // Массив
array[0] = 'Wayne'; // Error, -> Index signature in type 'readonly string[]' only permits reading.

array[array.length] = 'Batman'; // Error -> Index signature in type 'readonly string[]' only permits reading.

Помимо этого, у readonly массива отсутствуют методы, с помощью которых можно изменить элементы массива.

let array: readonly string[] = ['Kent', 'Clark'];
array.push('Batman'); // Error -> Property 'push' does not exist on type 'readonly string[]'.
array.shift(); // Error -> Property 'shift' does not exist on type 'readonly string[]'.

array.indexOf('Kent'); // Ok
array.map(item => item); // Ok

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

let tuple: readonly [string, string] = ['Kent', 'Clark'];

tuple[0] = 'Wayne'; // Error -> Cannot assign to '0' because it is a read-only property.
tuple.push('Batman'); // Error -> Property 'push' does not exist on type 'readonly [string, string]'.
tuple.shift(); // Error -> Property 'shift' does not exist on type 'readonly [string, string]'.

tuple.indexOf('Kent'); // Ok
tuple.map(item => item); // Ok

Readonly<T>

Также не будет лишним упомянуть, что массив или кортеж указанный в аннотации с помощью расширенного типа Readonly<T>, расценивается выводом типов как помеченный модификатором readonly .

// type A = readonly number[];
type A = Readonly<number[]>;

// type B = readonly [string, boolean];
type B = Readonly<[string, boolean]>;

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

function concat<
  T extends readonly unknown[],
  U extends readonly unknown[]
> (a: T, b: U): [...T, ...U] {
  return [...a, ...b]
}

const v0 = concat([0, 1], [2, 3]) // number[]

const v1 = concat([0, 1] as const, [2, 3] as const) // [0, 1, 2, 3]

const v2 = concat([0, 1] as const, [2, 3]) // [0, 1, ...number[]]

const v3 = concat([0, 1], [2, 3] as const) // number[]

ReadonlyArray<T>

Расширенный тип ReadonlyArray предназначен для создания неизменяемых массивов. ReadonlyArray запрещает изменять значения массива, используя индексную сигнатуру array[n] .

let array: ReadonlyArray<number> = [0, 1, 2];

array[0] = 1; // Error -> Index signature in type 'readonly number[]' only permits reading.
array[array.length] = 3; // Error -> Index signature in type 'readonly number[]' only permits reading.

Кроме того, тип ReadonlyArray не содержит методы, способные изменить, удалить или добавить элементы.

let array: ReadonlyArray<number> = [0, 1, 2];

array.push(3); // Error -> Property 'push' does not exist on type 'readonly number[]'.
array.shift(); // Error -> Property 'shift' does not exist on type 'readonly number[]'.

array.indexOf(0); // Ok

ReadonlyMap<K, V> (неизменяемая карта)

Расширенный тип ReadonlyMap , в отличие от своего полноценного прототипа, не имеет методов, способных его изменить.

let map: ReadonlyMap<string, number> = new Map([["key", 0]]);

ReadonlySet<T> (неизменяемое множество)

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

let set: ReadonlySet<number> = new Set([0, 1, 2]);

6. Модификатор override

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

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

class SuperClass {
  /**
  * [*] Определяет метод
  */
  a(){} // [*]
  b(){} // [*]
}
class SubClass extends SuperClass {
  /**
  * [*] Переопределяет методы своего суперкласса.
  */
  a(){} // [*]
  b(){} // [*]
}

Всё банально просто! Но что, если над проектом работает большое количество команд находящихся в разных уголках земного шара и команде занимающейся разработкой SuperClass неожиданно придеёт в голову изменить его api удалив оба метода? В таком случае, разработчики класса SubClass даже не узнают об этом, поскольку переопределение превратится в определение. Другими словами, компилятор даже глазом не моргнет, поскольку посчитает, что класс SubClass определят методы a() и b() .

class SuperClass {
  /**
  * Удалили a() и b() и добавили c().
  */
  с(){}
}
class SubClass extends SuperClass {
  /**
  * Ошибки не возникает, так как компилятор считает
  * что данный класс определяет оба метода.
  */
  a(){}
  b(){}
}

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

class SuperClass {
  /**
  * Удалили a() и b() и добавили c().
  */
  с(){}
}
class SubClass extends SuperClass {
  /**
  * [*] Error ->
  * This member cannot have an 'override' modifier
  * because it is not declared in the base class
  'SuperClass'.ts(4113)
  *
  * Теперь компилятор понимает, что происходи переопределение
  * несуществующих методов.
  */
  override a(){} // [*]
  override b(){} // [*]
}

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

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

class SuperClass {
  a(){}
  b(){}
}
class SubClass extends SuperClass {
  /**
  * --noImplicitOverride = true
  *
  * [*] Error -> This member must have an 'override'
  * modifier because it overrides a member in the base
  * class 'SuperClass'.ts(4114)
  */
  b(){} // [*]
}