Примитивы: stringnumber и boolean

В JS часто используется 3 примитиваstringnumber и boolean. Каждый из них имеет соответствующий тип в TS:

  • string представляет строковые значения, например, 'Hello World'
  • number предназначен для чисел, например, 42JS не различает целые числа и числа с плавающей точкой (или запятой), поэтому не существует таких типов, как int или float — только number
  • boolean — предназначен для двух значений: true и false

Обратите внимание: типы StringNumber и Boolean (начинающиеся с большой буквы) являются легальными и ссылаются на специальные встроенные типы, которые, однако, редко используются в коде. Для типов всегда следует использовать stringnumber или boolean.

Массивы

Для определения типа массива [1, 2, 3] можно использовать синтаксис number[]; такой синтаксис подходит для любого типа (например, string[] — это массив строк и т.д.). Также можно встретить Array<number>, что означает тоже самое. Такой синтаксис, обычно, используется для определения общих типов или дженериков (generics).

Обратите внимание[number] — это другой тип, кортеж (tuple).

any

TS предоставляет специальный тип any, который может использоваться для отключения проверки типов:

let obj: any = { x: 0 };
// Ни одна из строк ниже не приведет к возникновению ошибки на этапе компиляции
// Использование `any` отключает проверку типов
// Использование `any` означает, что вы знакомы со средой выполнения кода лучше, чем `TS`
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;

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

noImplicitAny

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

Обычно, мы хотим этого избежать, поскольку any является небезопасным с точки зрения системы типов. Установка флага noImplicitAny позволяет квалифицировать любое неявное any как ошибку.

Аннотации типа для переменных

При объявлении переменной с помощью constlet или var опционально можно определить ее тип:

const myName: string = "John";

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

// В аннотации типа нет необходимости - `myName` будет иметь тип `string`
const myName = "John";

Функции

В JS функции, в основном, используются для работы с данными. TS позволяет определять типы как для входных (input), так и для выходных (output) значений функции.

Аннотации типа параметров

При определении функции можно указать, какие типы параметров она принимает:

function greet(name: string) {
  console.log(`Hello, ${name.toUpperCase()}!`);
}

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

greet(42);
// Argument of type 'number' is not assignable to parameter of type 'string'. Аргумент типа 'number' не может быть присвоен параметру типа 'string'

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

Аннотация типа возвращаемого значения

Также можно аннотировать тип возвращаемого функцией значения:

function getFavouriteNumber(): number {
  return 26;
}

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

Анонимные функции

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

Вот пример:

// Аннотации типа отсутствуют, но это не мешает `TS` обнаруживать ошибки
const names = ["Alice", "Bob", "John"];
 
// Определение типов на основе контекста вызова функции
names.forEach(function (s) {
  console.log(s.toUppercase());
  // Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'? Свойства 'toUppercase' не существует в типе 'string'. Вы имели ввиду 'toUpperCase'?
});
 
// Определение типов на основе контекста также работает для стрелочных функций
names.forEach((s) => {
  console.log(s.toUppercase());
  // Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
});

Несмотря на отсутствие аннотации типа для sTS использует типы функции forEach, а также предполагаемый тип массива для определения типа s. Этот процесс называется определением типа на основе контекста (contextual typing).

Типы объекта

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

function printCoords(pt: { x: number; y: number }) {
  console.log(`Значение координаты 'x': ${pt.x}`);
  console.log(`Значение координаты 'y': ${pt.y}`);
}
 
printCoords({ x: 3, y: 7 });

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

Опциональные свойства

Для определения свойства в качестве опционального используется символ ? после названия свойства:

function printName(obj: { first: string; last?: string }) {
  // ...
}
// Обе функции скомпилируются без ошибок
printName({ first: "John" });
printName({ first: "Jane", last: "Air" });

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

function printName(obj: { first: string; last?: string }) {
  // Ошибка - приложение может сломаться, если аргумент `last` не будет передан в функцию
  console.log(obj.last.toUpperCase()); // Object is possibly 'undefined'. Потенциальным значением объекта является 'undefined'
 
  if (obj.last !== undefined) {
    // Теперь все в порядке
    console.log(obj.last.toUpperCase());
  }
 
  // Безопасная альтернатива, использующая современный синтаксис `JS` - оператор опциональной последовательности (`?.`)
  console.log(obj.last?.toUpperCase());
}

Объединения (#unions)

Обратите внимание: в литературе, посвященной TSunion, обычно, переводится как объединение, но фактически речь идет об альтернативных типах, объединенных в один тип.

Определение объединения

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

Реализуем функцию, которая может оперировать строками или числами:

function printId(id: number | string) {
  console.log(`Ваш ID: ${id}`);
}
 
// OK
printId(101);
// OK
printId("202");
// Ошибка
printId({ myID: 22342 });
// Argument of type '{ myID: number }' is not assignable to parameter of type 'string | number'. Type '{ myID: number }' is not assignable to type 'number'. Аргумент типа '{ myID: number }' не может быть присвоен параметру типа 'string | number'. Тип '{ myID: number }' не может быть присвоен типу 'number'

Работа с объединениями

В случае с объединениями, TS позволяет делать только такие вещи, которые являются валидными для каждого члена объединения. Например, если у нас имеется объединение string | number, мы не сможем использовать методы, которые доступны только для string:

function printId(id: number | string) {
  console.log(id.toUpperCase());
  // Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'.
}

Решение данной проблемы заключается в сужении (narrowing) объединения. Например, TS знает, что только для string оператор typeof возвращает 'string':

function printId(id: number | string) {
  if (typeof id === "string") {
    // В этой ветке `id` имеет тип 'string'
    console.log(id.toUpperCase());
  } else {
    // А здесь `id` имеет тип 'number'
    console.log(id);
  }
}

Другой способ заключается в использовании функции, такой как Array.isArray:

function welcomePeople(x: string[] | string) {
  if (Array.isArray(x)) {
    // Здесь `x` - это 'string[]'
    console.log("Привет, " + x.join(" и "));
  } else {
    // Здесь `x` - 'string'
    console.log("Добро пожаловать, одинокий странник " + x);
  }
}

В некоторых случаях все члены объединения будут иметь общие методы. Например, и массивы, и строки имеют метод slice. Если каждый член объединения имеет общее свойство, необходимость в сужении отсутствует:

function getFirstThree(x: number[] | string) {
  return x.slice(0, 3);
}

Синонимы типов (type#aliases)

Что если мы хотим использовать один и тот же тип в нескольких местах? Для этого используются синонимы типов:

type Point = {
  x: number;
  y: number;
};
 
// В точности тоже самое, что в приведенном выше примере
function printCoords(pt: Point) {
  console.log(`Значение координаты 'x': ${pt.x}`);
  console.log(`Значение координаты 'y': ${pt.y}`);
}
 
printCoords({ x: 3, y: 7 });

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

type ID = number | string;

Обратите внимание: синонимы — это всего лишь синонимы, мы не можем создавать на их основе другие “версии” типов. Например, такой код может выглядеть неправильным, но TS не видит в нем проблем, поскольку оба типа являются синонимами одного и того же типа:

type UserInputSanitizedString = string;
 
function sanitizeInput(str: string): UserInputSanitizedString {
  return sanitize(str);
}
 
// Создаем "обезвреженный" инпут
let userInput = sanitizeInput(getInput());
 
// По-прежнему имеем возможность изменять значение переменной
userInput = "new input";

Интерфейсы

Определение интерфейса#interface — это другой способ определения типа объекта:

interface Point {
  x: number;
  y: number;
}
 
function printCoords(pt: Point) {
  console.log(`Значение координаты 'x': ${pt.x}`);
  console.log(`Значение координаты 'y': ${pt.y}`);
}
 
printCoords({ x: 3, y: 7 });

TS иногда называют структурно типизированной системой типов (structurally typed type system) — TS заботит лишь соблюдение структуры значения, передаваемого в функцию printCoords, т.е. содержит ли данное значение ожидаемые свойства.

Разница между синонимами типов и интерфейсами

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

Пример расширения интерфейса:

interface Animal {
  name: string;
}
 
interface Bear extends Animal {
  honey: boolean;
}
 
const bear = getBear();
bear.name;
bear.honey;

Пример расширения типа с помощью пересечения (intersection):

type Animal {
 name: string
}
 
type Bear = Animal & {
 honey: boolean
}
 
const bear = getBear()
bear.name
bear.honey

Пример добавления новых полей в существующий интерфейс:

interface Window {
 title: string
}
 
interface Window {
 ts: TypeScriptAPI
}
 
const src = 'const a = 'Hello World''
window.ts.transpileModule(src, {})

Тип не может быть изменен после создания:

type Window = {
  title: string;
};
 
type Window = {
  ts: TypeScriptAPI;
};
// Ошибка: повторяющийся идентификатор 'Window'.

Общее правило: используйте interface до тех пор, пока вам не понадобятся возможности type.

Утверждение типа (#type-assertion )

В некоторых случаях мы знаем о типе значения больше, чем TS.

Например, когда мы используем document.getElementByIdTS знает лишь то, что данный метод возвращает какой-то HTMLElement, но мы знаем, например, что будет возвращен HTMLCanvasElement. В этой ситуации мы можем использовать утверждение типа для определения более конкретного типа:

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

Для утверждения типа можно использовать другой синтаксис (е в TSX-файлах):

const myCanvas = <HTMLCanvasElement>document.getElementById('main_canvas')

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

const x = "hello" as number;
// Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
// Преобразование типа 'string' в тип 'number' может быть ошибкой, поскольку эти типы не перекрываются. Если это было сделано намерено, то выражение сначала следует преобразовать в 'unknown'

Иногда это правило может быть слишком консервативным и мешать выполнению более сложных валидных преобразований. В этом случае можно использовать двойное утверждение: сначала привести тип к any (или unknown), затем к нужному типу:

const a = expr as any as T;

Литеральные типы (#literal-types )

В дополнение к общим типам string и number, мы можем ссылаться на конкретные строки и числа, находящиеся на определенных позициях.

Вот как TS создает типы для литералов:

let changingString = "Hello World";
changingString = "Olá Mundo";
// Поскольку `changingString` может представлять любую строку, вот
// как TS описывает ее в системе типов
changingString;
// let changingString: string
 
const constantString = "Hello World";
// Поскольку `constantString` может представлять только указанную строку, она
// имеет такое литеральное представление типа
constantString;
// const constantString: 'Hello World'

Сами по себе литеральные типы особой ценности не представляют:

let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";
// Type '"howdy"' is not assignable to type '"hello"'.

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

function printText(s: string, alignment: "left" | "right" | "center") {
  // ...
}
printText("Hello World", "left");
printText("G'day, mate", "centre");
// Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.

Числовые литеральные типы работают похожим образом:

function compare(a: string, b: string): -1 | 0 | 1 {
  return a === b ? 0 : a > b ? 1 : -1;
}

Разумеется, мы можем комбинировать литералы с нелитеральными типами:

interface Options {
  width: number;
}
function configure(x: Options | "auto") {
  // ...
}
configure({ width: 100 });
configure("auto");
configure("automatic");
// Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.

Предположения типов литералов

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

const obj = { counter: 0 };
if (someCondition) {
  obj.counter = 1;
}

TS не будет считать присвоение значения 1 полю, которое раньше имело значение 0, ошибкой. Это объясняется тем, что TS считает, что типом obj.counter является number, а не 0.

Тоже самое справедливо и в отношении строк:

const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);
// Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.

В приведенном примере предположительный типом req.method является string, а не 'GET'. Поскольку код может быть вычислен между созданием req и вызовом функции handleRequest, которая может присвоить req.method новое значение, например, GUESSTS считает, что данный код содержит ошибку.

Существует 2 способа решить эту проблему.

  1. Можно утвердить тип на каждой позиции:
// Изменение 1
const req = { url: "https://example.com", method: "GET" as "GET" };
// Изменение 2
handleRequest(req.url, req.method as "GET");
  1. Для преобразования объекта в литерал можно использовать as const:
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);

#null и #undefined

В JS существует два примитивных значения, сигнализирующих об отсутствии значения: null и undefinedTS имеет соответствующие типы. То, как эти типы обрабатываются, зависит от настройки strictNullChecks (см. часть 1).

Оператор утверждения ненулевого значения (non-null assertion operator)

TS предоставляет специальный синтаксис для удаления null и undefined из типа без необходимости выполнения явной проверки. Указание ! после выражения означает, что данное выражение не может быть нулевым, т.е. иметь значение null или undefined:

function liveDangerously(x?: number | undefined) {
  // Ошибки не возникает
  console.log(x!.toFixed());
}

Перечисления (#enums )

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

Редко используемые примитивы

#bigint

Данный примитив используется для представления очень больших целых чисел BigInt:

// Создание `bigint` с помощью функции `BigInt`
const oneHundred: bigint = BigInt(100);
 
// Создание `bigint` с помощью литерального синтаксиса
const anotherHundred: bigint = 100n;

Подробнее о BigInt можно почитать здесь.

#symbol

Данный примитив используется для создания глобально уникальных ссылок с помощью функции Symbol():

const firstName = Symbol("name");
const secondName = Symbol("name");
 
if (firstName === secondName) {
  // This condition will always return 'false' since the types 'typeof firstName' and 'typeof secondName' have no overlap.
  // Символы `firstName` и `lastName` никогда не будут равными
}

Подробнее о символах можно почитать здесь.