LEARN X · ЗА 15 МИН

TypeScript

TypeScript за 15 минут: типы, интерфейсы, дженерики, union, narrowing, классы и утилитные типы — экспресс-тур для тех, кто знает JavaScript.

TypeScript — это JavaScript со статическими типами. Тот же синтаксис, та же среда выполнения, но компилятор ловит ошибки до запуска. Этот тур предполагает, что вы знаете JS, и показывает только то, что добавляет TypeScript. Весь язык — в комментариях кода ниже.

1. Зачем TypeScript и базовые типы

Типы пишутся после двоеточия. Компилятор выводит их сам, но аннотации делают код самодокументируемым.

// Аннотация типа: имя: Тип
let username: string = "Ада";
let age: number = 36;          // одно число и для int, и для float
let isAdmin: boolean = false;

// Вывод типов (inference): TS сам понимает тип из значения
let city = "Лондон";           // city: string автоматически
// city = 42;                  // Ошибка: number не присваивается string

// Спецтипы значений
let nothing: null = null;
let missing: undefined = undefined;
let huge: bigint = 9007199254740993n;
let sym: symbol = Symbol("id");

// Шаблонные строки и операторы — как в JS, но с проверкой типов
const greeting: string = `Привет, ${username}, тебе ${age}`;

// Компилируется командой: tsc file.ts -> file.js (типы стираются)

2. Массивы и кортежи

// Массив: Тип[] или Array<Тип>
let nums: number[] = [1, 2, 3];
let names: Array<string> = ["Ани", "Боб"];
nums.push(4);
// nums.push("пять");          // Ошибка: ожидался number

// Массив с несколькими типами через union
let mixed: (number | string)[] = [1, "два", 3];

// Кортеж (tuple): фиксированная длина и тип каждой позиции
let point: [number, number] = [10, 20];
let pair: [string, number] = ["возраст", 36];
// pair = [36, "возраст"];     // Ошибка: порядок типов нарушен

// Именованные кортежи (для читаемости)
let rgb: [red: number, green: number, blue: number] = [255, 128, 0];

// Readonly-кортеж нельзя мутировать
const origin: readonly [number, number] = [0, 0];
// origin[0] = 5;              // Ошибка: только для чтения

3. Объекты и интерфейсы

interface описывает форму объекта.

interface User {
  id: number;
  name: string;
  email?: string;          // ? — поле опционально (может отсутствовать)
  readonly createdAt: Date; // readonly — задаётся один раз
}

const u: User = {
  id: 1,
  name: "Ада",
  createdAt: new Date(),
  // email можно не указывать
};
// u.createdAt = new Date();  // Ошибка: readonly

// Интерфейсы расширяются через extends
interface Admin extends User {
  role: "admin";
}

// Index signature — произвольные ключи известного типа
interface Dictionary {
  [key: string]: number;
}
const scores: Dictionary = { math: 5, history: 4 };

4. Type aliases и объединения

// type — псевдоним для любого типа (не только объекта)
type ID = number | string;          // union: число ИЛИ строка
let userId: ID = 42;
userId = "abc-42";                   // тоже валидно

// Литеральные типы: значение само становится типом
type Direction = "north" | "south" | "east" | "west";
let dir: Direction = "north";
// dir = "up";                       // Ошибка: не входит в union

// type умеет описывать объекты (как interface)
type Point = { x: number; y: number };

// Пересечение типов через & (объединяет поля)
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;          // { name: string; age: number }

// interface vs type: interface расширяется и сливается,
// type гибче (union, кортежи, mapped-типы). Для объектов — на вкус.

5. Функции

// Типы параметров и возвращаемого значения
function add(a: number, b: number): number {
  return a + b;
}

// Опциональный (?) и параметр по умолчанию
function greet(name: string, greeting: string = "Привет"): string {
  return `${greeting}, ${name}`;
}
function log(msg: string, level?: string): void {
  // void — функция ничего не возвращает
  console.log(level ?? "info", msg);
}

// Rest-параметры
function sum(...nums: number[]): number {
  return nums.reduce((a, b) => a + b, 0);
}

// Тип функции как значение
const multiply: (a: number, b: number) => number = (a, b) => a * b;

// Перегрузки (overloads): несколько сигнатур + одна реализация
function parse(x: string): string[];
function parse(x: number): number;
function parse(x: string | number): string[] | number {
  return typeof x === "string" ? x.split("") : x * 2;
}

6. Enum и литеральные типы

// Числовой enum (по умолчанию 0, 1, 2...)
enum Status {
  Active,        // 0
  Inactive,      // 1
  Banned,        // 2
}
let s: Status = Status.Active;

// Строковый enum (явные значения — читается в рантайме)
enum Color {
  Red = "RED",
  Green = "GREEN",
  Blue = "BLUE",
}

// const enum — инлайнится компилятором, нет объекта в рантайме
const enum Size { S, M, L }
let sz = Size.M;          // подставится число 1

// Часто union литералов лучше enum: легче и tree-shakeable
type Theme = "light" | "dark";
let theme: Theme = "dark";

7. Дженерики

Дженерики — параметры типа <T>: код работает с любым типом, сохраняя проверки.

// T — переменная типа, подставляется при вызове
function identity<T>(value: T): T {
  return value;
}
const a = identity<string>("hi");   // T = string
const b = identity(42);             // T = number (выведен сам)

// Дженерик-интерфейс
interface Box<T> {
  value: T;
}
const boxed: Box<number> = { value: 10 };

// Ограничение extends: T обязан иметь поле length
function longest<T extends { length: number }>(x: T, y: T): T {
  return x.length >= y.length ? x : y;
}
longest("abc", "de");               // строки ок (есть length)
longest([1, 2], [3]);               // массивы ок
// longest(1, 2);                   // Ошибка: у number нет length

// Несколько параметров типа
function pair<K, V>(key: K, value: V): [K, V] {
  return [key, value];
}

8. Сужение типов (narrowing)

Внутри union TS сужает тип по проверкам — и вы получаете точные подсказки.

// typeof — для примитивов
function format(x: string | number): string {
  if (typeof x === "string") {
    return x.toUpperCase();   // здесь x: string
  }
  return x.toFixed(2);        // здесь x: number
}

// in — проверка наличия поля
type Cat = { meow: () => void };
type Dog = { bark: () => void };
function speak(pet: Cat | Dog): void {
  if ("meow" in pet) pet.meow();
  else pet.bark();
}

// instanceof — для классов
function len(x: Date | string): number {
  return x instanceof Date ? x.getFullYear() : x.length;
}

// Пользовательский type guard: возвращаемый тип `arg is Type`
function isCat(pet: Cat | Dog): pet is Cat {
  return "meow" in pet;
}
if (isCat({ meow: () => {} })) { /* тип сужен до Cat */ }

9. Классы

interface Greetable {
  greet(): string;          // контракт, который класс реализует
}

class Account implements Greetable {
  public name: string;      // доступно везде (по умолчанию)
  private balance: number;  // только внутри класса
  protected pin: number;    // класс + наследники
  readonly id: number;      // нельзя менять после создания

  // Краткая запись: модификатор в конструкторе создаёт поле
  constructor(name: string, balance: number, public currency: string = "RUB") {
    this.name = name;
    this.balance = balance;
    this.pin = 0;
    this.id = Math.random();
  }

  greet(): string {
    return `Счёт ${this.name}: ${this.balance} ${this.currency}`;
  }

  // Геттер
  get info(): string {
    return this.greet();
  }
}

class Savings extends Account {
  withdraw(): void {
    // this.balance;          // Ошибка: private недоступен
    console.log(this.pin);    // protected — можно
  }
}

10. Утилитные типы

Встроенные дженерики, которые трансформируют существующие типы.

interface Todo {
  id: number;
  title: string;
  done: boolean;
}

// Partial<T> — все поля опциональны (удобно для обновлений)
function update(todo: Todo, patch: Partial<Todo>): Todo {
  return { ...todo, ...patch };
}

// Pick<T, K> — оставить только указанные поля
type TodoPreview = Pick<Todo, "id" | "title">;

// Omit<T, K> — убрать указанные поля
type TodoBody = Omit<Todo, "id">;   // { title; done }

// Record<K, V> — объект с ключами K и значениями V
type Pages = Record<string, number>;
const pages: Pages = { home: 1, about: 2 };

// Readonly<T> — все поля только для чтения
const frozen: Readonly<Todo> = { id: 1, title: "x", done: false };
// frozen.done = true;        // Ошибка

// Прочие: Required<T>, ReturnType<F>, Parameters<F>, Awaited<P>

11. any / unknown / never и null-безопасность

// any — отключает проверки (избегайте: теряете весь смысл TS)
let loose: any = 5;
loose.foo.bar();             // компилятор молчит — опасно

// unknown — безопасная альтернатива: сначала сузь тип
let safe: unknown = JSON.parse("{}");
// safe.foo;                 // Ошибка: нужно проверить тип
if (typeof safe === "string") safe.toUpperCase();

// never — значение, которого не бывает (исчерпывающие проверки)
function fail(msg: string): never {
  throw new Error(msg);      // функция не возвращает управление
}

// strictNullChecks: null/undefined не входят в типы по умолчанию.
// Нужно объявить их явно через union:
function find(arr: string[], q: string): string | undefined {
  return arr.find((x) => x === q);
}
const res = find(["a"], "b");
// res.length;               // Ошибка: res может быть undefined

// Опциональная цепочка ?. и ?? (nullish coalescing)
const n = res?.length ?? 0;  // если undefined -> 0

// Non-null assertion ! — "я уверен, что не null" (на свой риск)
const sure = find(["a"], "a")!.length;

12. Дженерик-утилиты на практике

Типизация ответа API — где дженерики реально окупаются.

// Обёртка ответа: T — тип полезных данных
interface ApiResponse<T> {
  data: T;
  status: number;
  error: string | null;
}

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

// fetch с типизацией: возвращает Promise нужной формы
async function getJson<T>(url: string): Promise<ApiResponse<T>> {
  const res = await fetch(url);
  const data = (await res.json()) as T;   // приведение типа через as
  return { data, status: res.status, error: null };
}

// Вызов: подставляем конкретный тип — дальше всё проверено
async function main(): Promise<void> {
  const r = await getJson<User[]>("/api/users");
  r.data.forEach((u) => console.log(u.name)); // u: User — есть .name

  const single = await getJson<User>("/api/users/1");
  console.log(single.data.id);                // .id типизирован
}

// Итог: один дженерик ApiResponse<T> даёт точные типы для
// любого эндпоинта без дублирования кода. В этом сила TypeScript.
Поддержать проект