TypeScript_Core

TypeScript Core

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

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

greet("Анна");
// greet(42); // ошибка типов

TypeScript не проверяет реальные ответы API, JSON и пользовательский ввод во время выполнения. Внешние данные нужно валидировать отдельно.


Установка

npm install -D typescript
npx tsc --init
npx tsc --noEmit
{
  "scripts": {
    "typecheck": "tsc --noEmit"
  }
}

Расширения: .ts — TypeScript, .tsx — TypeScript с JSX, .d.ts — декларации типов.


Базовые типы и аннотации

Аннотация записывается после имени переменной, параметра или функции:

let username: string = "alex";
let age: number = 28;
let active: boolean = true;

function add(a: number, b: number): number {
  return a + b;
}

TypeScript часто выводит тип самостоятельно:

const title = "TypeScript"; // string
const count = 10;           // number
const enabled = true;       // boolean

Не стоит явно указывать очевидный тип. Аннотации особенно полезны для параметров, публичных контрактов, пустых коллекций и nullable-значений.

Основные типы

const text: string = "hello";
const price: number = 99.5;
const visible: boolean = true;
const large: bigint = 9_007_199_254_740_993n;
const key: symbol = Symbol("key");

let selected: string | null = null;
let missing: undefined = undefined;

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

const ids: number[] = [1, 2, 3];
const names: Array<string> = ["Анна", "Иван"];
const point: [number, number] = [55.75, 37.62];
const response: [status: number, message: string] = [200, "OK"];

Массив только для чтения:

const roles: readonly string[] = ["admin", "editor"];
// roles.push("user"); // ошибка

Объектные типы

const user: { id: number; name: string; active: boolean } = {
  id: 1,
  name: "Мария",
  active: true,
};

Повторно используемую структуру обычно выносят в interface или type.

any, unknown, void, never

any отключает проверку типов:

let data: any = "text";
data.missingMethod(); // компилятор не остановит ошибку

unknown принимает любое значение, но требует проверки:

function printValue(value: unknown): void {
  if (typeof value === "string") {
    console.log(value.toUpperCase());
  }
}

void обычно обозначает отсутствие используемого результата. never — значение, которое не может возникнуть:

function log(message: string): void {
  console.log(message);
}

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

Литеральные типы

type Theme = "light" | "dark" | "system";
let theme: Theme = "dark";
// theme = "blue"; // ошибка

Сужение типов

Перед использованием union-типа нужно уточнить конкретный вариант:

function format(value: string | number): string {
  if (typeof value === "number") {
    return value.toFixed(2);
  }

  return value.trim();
}

Для сужения применяются typeof, instanceof, оператор in, сравнение литеральных полей и пользовательские проверки.

function getErrorMessage(error: unknown): string {
  return error instanceof Error
    ? error.message
    : "Неизвестная ошибка";
}

Type guard:

type User = { id: number; name: string };

function isUser(value: unknown): value is User {
  if (typeof value !== "object" || value === null) return false;

  const item = value as Record<string, unknown>;
  return typeof item.id === "number" && typeof item.name === "string";
}

Приведение as User не проверяет данные во время выполнения.


Интерфейсы

Интерфейс описывает форму объекта или контракт класса:

interface User {
  readonly id: number;
  name: string;
  email?: string;
}

const user: User = {
  id: 1,
  name: "Анна",
};

? делает свойство опциональным. readonly запрещает присваивание на этапе проверки типов, но не замораживает объект в JavaScript.

Расширение

interface Person {
  name: string;
}

interface Employee extends Person {
  department: string;
}

Интерфейс может расширять несколько интерфейсов и объединяться с другой декларацией того же имени.

Индексная сигнатура

interface Dictionary {
  [key: string]: string;
}

const labels: Dictionary = {
  save: "Сохранить",
  cancel: "Отмена",
};

Псевдонимы типов

type создаёт имя для любого типа:

type UserId = string | number;
type Coordinates = [number, number];
type Status = "idle" | "loading" | "success" | "error";

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

interface или type

Возможность interface type
Объектный контракт Да Да
Расширение extends &
Union и кортеж Нет Да
Слияние деклараций Да Нет
Utility/условные типы Ограниченно Да

interface удобен для расширяемых объектных контрактов и классов. type — для union, intersection, кортежей, функций и преобразований типов. Важнее придерживаться последовательного стиля проекта.


Union и Intersection

Union | означает «один из вариантов»:

type Id = string | number;

function normalizeId(id: Id): string {
  return typeof id === "number" ? String(id) : id.toLowerCase();
}

Дискриминируемое объединение удобно для состояний:

type RequestState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; message: string };
function renderState(state: RequestState<string[]>): string {
  switch (state.status) {
    case "idle": return "Ожидание";
    case "loading": return "Загрузка";
    case "success": return `Элементов: ${state.data.length}`;
    case "error": return state.message;
  }
}

Intersection & объединяет требования типов:

type Entity = { id: number };
type Timestamped = { createdAt: string };
type Article = Entity & Timestamped & { title: string };

Объект Article обязан содержать поля всех частей пересечения.


Enum

enum Direction {
  Up,
  Right,
  Down,
  Left,
}

enum UserRole {
  Admin = "admin",
  Editor = "editor",
  Viewer = "viewer",
}

Числовые enum по умолчанию начинаются с 0. Строковые значения удобнее при логировании и передаче данных.

Частая альтернатива — объект с as const:

const UserRole = {
  Admin: "admin",
  Editor: "editor",
  Viewer: "viewer",
} as const;

type UserRole = (typeof UserRole)[keyof typeof UserRole];

Такой вариант остаётся обычным JavaScript-объектом и позволяет получить union его значений.


Типизация функций

type Operation = (a: number, b: number) => number;

const multiply: Operation = (a, b) => a * b;

Опциональные параметры и значения по умолчанию:

function greet(name: string, title?: string): string {
  return title ? `${title} ${name}` : name;
}

function createPage(page = 1, pageSize = 20): string {
  return `?page=${page}&pageSize=${pageSize}`;
}

Rest-параметры:

function sum(...values: number[]): number {
  return values.reduce((total, value) => total + value, 0);
}

Callback:

function filterNumbers(
  values: number[],
  predicate: (value: number) => boolean,
): number[] {
  return values.filter(predicate);
}

Асинхронная функция возвращает Promise:

async function loadUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json() as Promise<User>;
}

Generic-параметр и as здесь не валидируют фактический JSON.

Перегрузка

function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
  return typeof value === "number" ? value.toFixed(2) : value.trim();
}

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


Дженерики

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

function identity<T>(value: T): T {
  return value;
}

const text = identity("hello"); // string
const count = identity(42);     // number

Работа с массивом:

function first<T>(items: T[]): T | undefined {
  return items[0];
}

Несколько параметров типа:

function createPair<K, V>(key: K, value: V): [K, V] {
  return [key, value];
}

Generic-структура:

interface ApiResponse<T> {
  data: T;
  status: number;
  message?: string;
}

type Paginated<T> = {
  items: T[];
  page: number;
  total: number;
};

Ограничение через extends:

function getLength<T extends { length: number }>(value: T): number {
  return value.length;
}

getLength("hello");
getLength([1, 2, 3]);

Типобезопасный доступ к свойству:

function getProperty<T, K extends keyof T>(object: T, key: K): T[K] {
  return object[key];
}

const user = { id: 1, name: "Анна", active: true };
const name = getProperty(user, "name"); // string

Дженерик нужен, когда связывает несколько типов. Если функция только принимает неизвестное значение, часто достаточно unknown.


Utility Types

Исходный тип:

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  active: boolean;
}

Partial<T> и Required<T>

type UserPatch = Partial<User>;
type CompleteUser = Required<User>;

Partial делает свойства опциональными, Required — обязательными. Преобразования неглубокие.

Readonly<T>

type ImmutableUser = Readonly<User>;

Запрещает присваивание свойствам на этапе компиляции.

Pick<T, K> и Omit<T, K>

type UserPreview = Pick<User, "id" | "name" | "active">;
type PublicUser = Omit<User, "password">;

Pick оставляет выбранные свойства, Omit исключает их.

Record<K, V>

type Role = "admin" | "editor" | "viewer";

type Permissions = Record<Role, string[]>;

const permissions: Permissions = {
  admin: ["read", "write", "delete"],
  editor: ["read", "write"],
  viewer: ["read"],
};

Другие utility types

type Status = "idle" | "loading" | "success" | "error";

type Finished = Exclude<Status, "idle" | "loading">;
type Pending = Extract<Status, "idle" | "loading">;
type Name = NonNullable<string | null | undefined>;

Типы функций:

function createUser() {
  return { id: 1, name: "Анна" };
}

type CreatedUser = ReturnType<typeof createUser>;
type FactoryArguments = Parameters<typeof createUser>;
type ResolvedUser = Awaited<Promise<CreatedUser>>;

Также доступны ConstructorParameters<T> и InstanceType<T>.


typeof, keyof, as const, satisfies

const settings = {
  theme: "dark",
  pageSize: 20,
};

type Settings = typeof settings;
type SettingsKey = keyof Settings; // "theme" | "pageSize"
type PageSize = Settings["pageSize"]; // number

as const сохраняет литеральные значения:

const routes = {
  home: "/",
  users: "/users",
} as const;

type Route = (typeof routes)[keyof typeof routes];
// "/" | "/users"

satisfies проверяет контракт, сохраняя точный выведенный тип значения:

type RouteName = "home" | "users";

const routes = {
  home: "/",
  users: "/users",
} satisfies Record<RouteName, string>;

Настройка tsconfig.json

Пример для React-приложения с современным сборщиком:

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "react-jsx",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

Основные параметры:

Параметр Назначение
target Версия выходного JavaScript
lib Типы стандартных API среды
module Формат модулей
moduleResolution Алгоритм поиска модулей
jsx Обработка JSX
strict Набор строгих проверок
noEmit Проверка без генерации JS
rootDir / outDir Исходная и выходная папки
paths Алиасы импортов
include / exclude Файлы проекта

paths нужно также настроить в сборщике и тестовой среде: один tsconfig.json не меняет импорты во время выполнения.

Для Node.js без сборщика часто используют:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "rootDir": "src",
    "outDir": "dist",
    "sourceMap": true
  },
  "include": ["src/**/*.ts"]
}
npm install -D @types/node

Типизация React

Компоненты с JSX хранятся в .tsx.

Props

type UserCardProps = {
  name: string;
  age?: number;
  active: boolean;
};

export function UserCard({ name, age, active }: UserCardProps) {
  return (
    <article>
      <h2>{name}</h2>
      {age !== undefined && <p>Возраст: {age}</p>}
      <p>{active ? "Активен" : "Неактивен"}</p>
    </article>
  );
}

children

import type { ReactNode } from "react";

type CardProps = {
  title: string;
  children: ReactNode;
};

function Card({ title, children }: CardProps) {
  return <section><h2>{title}</h2>{children}</section>;
}

Стандартные HTML-props

import type { ComponentPropsWithoutRef } from "react";

type ButtonProps = ComponentPropsWithoutRef<"button"> & {
  variant?: "primary" | "secondary";
};

function Button({ variant = "primary", ...props }: ButtonProps) {
  return <button className={`button--${variant}`} {...props} />;
}

useState

const [count, setCount] = useState(0);
const [query, setQuery] = useState("");
const [user, setUser] = useState<User | null>(null);
const [users, setUsers] = useState<User[]>([]);

Для пустого массива и null обычно нужен явный generic.

Состояние запроса:

type LoadState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; message: string };

const [state, setState] = useState<LoadState<User[]>>({
  status: "idle",
});

События

import type { ChangeEvent, FormEvent } from "react";

function SearchForm() {
  const [query, setQuery] = useState("");

  function handleChange(event: ChangeEvent<HTMLInputElement>) {
    setQuery(event.currentTarget.value);
  }

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={query} onChange={handleChange} />
    </form>
  );
}

В inline-обработчике тип события обычно выводится автоматически.

useRef

const inputRef = useRef<HTMLInputElement>(null);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

DOM-ссылка изначально равна null, потому что элемент ещё не смонтирован.

useReducer

type State = { count: number };
type Action =
  | { type: "increment" }
  | { type: "set"; value: number };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "increment": return { count: state.count + 1 };
    case "set": return { count: action.value };
  }
}

Context

import { createContext, useContext, type ReactNode } from "react";

type Theme = "light" | "dark";
type ThemeContextValue = {
  theme: Theme;
  setTheme: (theme: Theme) => void;
};

const ThemeContext = createContext<ThemeContextValue | null>(null);

function useTheme(): ThemeContextValue {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error("useTheme используется вне ThemeProvider");
  }
  return context;
}

Типизированный кастомный хук

function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(initialValue);

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue] as const;
}

as const сохраняет результат как кортеж. Данные, прочитанные из localStorage, всё равно нужно проверять во время выполнения.


Типизация API

Удобно разделять сетевую и доменную модели:

type UserDto = {
  id: number;
  full_name: string;
  created_at: string;
};

type User = {
  id: number;
  name: string;
  createdAt: Date;
};

function mapUser(dto: UserDto): User {
  return {
    id: dto.id,
    name: dto.full_name,
    createdAt: new Date(dto.created_at),
  };
}

Безопасная граница данных:

async function fetchJson(url: string): Promise<unknown> {
  const response = await fetch(url);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

async function loadUser(id: number): Promise<User> {
  const value = await fetchJson(`/api/users/${id}`);
  if (!isUser(value)) throw new Error("Некорректный формат ответа");
  return { ...value, createdAt: new Date() };
}

В реальном проекте type guard должен проверять все поля контракта; также можно применять библиотеки схемной валидации.


Частые ошибки

Злоупотребление any

any распространяется по коду и скрывает ошибки. Для неизвестных данных предпочтительнее unknown с проверкой.

Необоснованный as

const user = response as User;

Такая запись ничего не проверяет. Не используйте двойное приведение as unknown as T, чтобы просто подавить ошибку.

Оператор ! без гарантии

const element = document.querySelector("#app")!;

Оператор не создаёт элемент. Безопаснее проверить значение и выбросить понятную ошибку.

Противоречивые состояния

Вместо нескольких опциональных полей loading?, data?, error? используйте дискриминируемый union.

Игнорирование undefined

Обращение к отсутствующему индексу массива возвращает undefined. Опция noUncheckedIndexedAccess помогает учитывать это в типах.

Типы вместо валидации и тестов

TypeScript не гарантирует корректность бизнес-логики и внешних данных. Типы дополняют runtime-валидацию и тесты, а не заменяют их.


Рекомендации


Краткая памятка

interface User {
  readonly id: number;
  name: string;
  email?: string;
}

type Id = string | number;
type Admin = User & { permissions: string[] };

type UserPatch = Partial<User>;
type UserPreview = Pick<User, "id" | "name">;
type EditableUser = Omit<User, "id">;
type UsersById = Record<string, User>;

function first<T>(items: T[]): T | undefined {
  return items[0];
}

function getProperty<T, K extends keyof T>(object: T, key: K): T[K] {
  return object[key];
}

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