← назад к разделу

TypeScript — это JavaScript, к которому добавили типы. Типы — это правила о том, какие значения может принимать переменная, аргумент или результат функции. Компилятор проверяет эти правила до запуска кода и подсказывает прямо в редакторе. Разберём базовые типы по порядку: с чего начинать «с нуля» бэкенд-разработчику.

Зачем TypeScript поверх JavaScript

В JavaScript ошибки в работе с данными всплывают только во время выполнения. Опечатался в имени поля, передал строку туда, где ждали число, забыл, что функция иногда возвращает undefined — и узнаёшь об этом, когда сервис уже упал на проде.

TypeScript добавляет слой проверок до запуска. Компилятор tsc читает аннотации типов и ругается на несовместимости заранее.

function double(n: number): number {
  return n * 2;
}

double("10"); // ошибка ещё до запуска: строка вместо number

Важная деталь: типы существуют только на этапе компиляции. После сборки они стираются, и в рантайме выполняется обычный JavaScript — никаких типов в готовом коде нет. То есть TypeScript защищает разработчика, но не проверяет данные, пришедшие извне (из HTTP-запроса, из базы). Внешние данные всё равно нужно валидировать руками.

Короткая формула: TypeScript = JavaScript + проверка типов на этапе сборки.

Аннотации типов

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

const port: number = 8080;
const host: string = "localhost";
const debug: boolean = false;

function greet(name: string): string {
  return `Привет, ${name}`;
}

Базовые типы данных: number (целые и дробные — отдельного типа для целых нет), string, boolean, null, undefined. Массивы записываются как number[] или string[]. Объекты описываются перечислением полей.

const ids: number[] = [1, 2, 3];

const user: { id: number; name: string } = {
  id: 1,
  name: "Аня",
};

Вывод типов

Аннотации не нужны везде. TypeScript умеет сам выводить тип из значения — это называется type inference (вывод типов).

const port = 8080;       // выведен number
const host = "localhost"; // выведен string
const ids = [1, 2, 3];    // выведен number[]

Здесь port всё равно имеет тип number, хотя аннотации нет: компилятор посмотрел на значение справа. Поэтому аннотации обычно ставят там, где вывод невозможен или ненадёжен — на аргументах функций и иногда на её результате. Внутри тела функции и для локальных переменных чаще полагаются на вывод.

Короткая формула: аннотируй границы (аргументы, возвращаемое значение), внутри доверяй выводу.

type и interface

Когда форма объекта повторяется, ей дают имя. Есть два способа: type и interface.

interface User {
  id: number;
  name: string;
}

type Product = {
  id: number;
  price: number;
};

Оба описывают форму объекта, и в этом случае они почти взаимозаменяемы. Разница в деталях:

  • interface можно расширять через extends и дополнять повторным объявлением с тем же именем (объявления сливаются). Это удобно для публичных контрактов и описаний из библиотек.
  • type умеет больше: он даёт имя не только объекту, но и любому типу — union, пересечению, кортежу, примитиву.
type Id = number;                 // псевдоним для примитива
type Point = [number, number];    // кортеж
type Status = "active" | "banned"; // union (см. ниже)

Практичное правило для начала: для формы объекта бери interface, для всего остального (union, псевдонимы, комбинации) — type. Команды часто выбирают что-то одно для единообразия — это тоже нормально.

Объединения и пересечения

Объединение (union) через | означает «либо то, либо это».

type Id = number | string;

function findUser(id: number | string) {
  // id может быть и числом, и строкой
}

Частый случай — значение, которого может не быть:

let token: string | null = null;
token = "abc123";

Пересечение (intersection) через & означает «и то, и это одновременно» — объединяет поля нескольких типов в один.

interface HasId {
  id: number;
}
interface HasTimestamps {
  createdAt: Date;
}

type Entity = HasId & HasTimestamps;
// Entity имеет и id, и createdAt

const row: Entity = {
  id: 1,
  createdAt: new Date(),
};

Литеральные типы и опциональные поля

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

type Role = "admin" | "user" | "guest";

function setRole(role: Role) {
  // сюда нельзя передать произвольную строку,
  // только одно из трёх значений
}

setRole("admin");  // ок
setRole("owner");  // ошибка: нет такого варианта

Опциональное поле помечается знаком ? — его можно не указывать. Тип такого поля автоматически становится «значение или undefined».

interface CreateUser {
  name: string;
  email?: string; // необязательное поле
}

const a: CreateUser = { name: "Аня" };               // ок
const b: CreateUser = { name: "Боб", email: "b@x.io" }; // тоже ок

any, unknown и never

Три специальных типа, которые важно различать.

any — «отключи проверку для этого значения». С ним можно делать что угодно, и компилятор молчит. Это опасно: any заражает соседний код и сводит на нет всю пользу TypeScript — ошибки снова всплывают только в рантайме.

let data: any = fetchSomething();
data.foo.bar.baz;       // ошибки не будет на сборке — упадёт в рантайме
const n: number = data; // тоже пропустит

unknown — безопасная альтернатива. Это тоже «неизвестно что», но компилятор не даёт ничего с таким значением сделать, пока ты не докажешь его тип проверкой.

let data: unknown = fetchSomething();

// data.foo;  // ошибка: тип ещё неизвестен

if (typeof data === "string") {
  data.toUpperCase(); // здесь уже безопасно: внутри проверки это string
}

Поэтому для внешних данных (тело запроса, ответ стороннего API) бери unknown, а не any, и проверяй тип перед использованием. Как именно сужать тип проверками — отдельная тема, см. ссылки ниже.

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

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

Короткая формула: any — выключает типы (избегай), unknown — безопасный «пока не знаю», never — значения не бывает.

Коротко

  • TypeScript добавляет к JavaScript проверку типов на этапе сборки; в рантайме типы стираются, внешние данные всё равно надо валидировать.
  • Аннотация : тип задаёт тип явно; вывод типов (inference) часто делает это за тебя — аннотируй границы функций, внутри доверяй выводу.
  • interface — для формы объекта (расширяется, сливается); type — для union, пересечений, псевдонимов и всего остального.
  • union (|) — «либо одно, либо другое»; intersection (&) — «всё сразу».
  • Литеральные типы в union задают набор допустимых значений; ? делает поле опциональным.
  • any отключает проверки и опасен; unknown — безопасная замена с обязательной проверкой; never — тип без значений.

Что почитать дальше

  • Что нужно знать из JavaScript — база языка, поверх которой ложатся типы.
  • Сужение типов и type guards — как безопасно работать с union и unknown.
  • Дженерики (generics) — как описывать типы, работающие с любыми данными.