Skip to main content
typescriptBeginner60 минут

TypeScript для JavaScript-разработчиков

Полное руководство по переходу с JavaScript на TypeScript: типы, интерфейсы, generics, утилитные типы и интеграция с React/Next.js

#typescript#javascript#react#nextjs#types#interfaces#generics#utility types

TypeScript для JavaScript-разработчиков

TypeScript — это не новый язык, а JavaScript с суперспособностями. Всё, что работает в JS, работает в TS. Только безопаснее.

Как пользоваться материалом

  • Спешишь? Прочти введение, блок про базовые типы и интеграцию с React.
  • Пишешь на JS? Каждый пример покажет, как было и как стало.
  • Хочешь без боли? Переходи постепенно — TypeScript поддерживает .js файлы в проекте.

Введение

TypeScript — это надстройка над JavaScript, которая добавляет статическую типизацию. После компиляции получается чистый JavaScript, который работает везде.

Зачем нужен TypeScript

  • Раннее обнаружение ошибок — IDE подсветит проблему до запуска кода
  • Автодополнение и рефакторинг — редактор знает, какие методы есть у объекта
  • Документация в коде — типы объясняют, что функция принимает и возвращает
  • Упрощение поддержки — через полгода ты поймешь свой же код

Как работает TypeScript

// Пишешь TypeScript
function greet(name: string): string {
  return `Привет, ${name}!`;
}
 
// Компилируется в JavaScript
function greet(name) {
  return `Привет, ${name}!`;
}

Типы существуют только на этапе разработки. В runtime их нет — это чистый JavaScript.

Ключевое правило: TypeScript — это JavaScript + типы. Любой валидный JS-код — валидный TS-код.

Базовые типы

Примитивные типы

// JavaScript
let name = "Иван";
let age = 25;
let isActive = true;
 
// TypeScript — с явными типами
let name: string = "Иван";
let age: number = 25;
let isActive: boolean = true;
 
// TypeScript с type inference (рекомендуется)
let name = "Иван"; // TS сам определит, что это string
let age = 25; // number
let isActive = true; // boolean

TypeScript умеет выводить типы автоматически (type inference). Не нужно писать очевидные аннотации — редактор покажет тип при наведении.

Массивы и объекты

// Массивы
const numbers: number[] = [1, 2, 3, 4, 5];
const names: string[] = ["Анна", "Борис", "Мария"];
 
// Альтернативный синтаксис (одно и то же)
const numbers: Array<number> = [1, 2, 3];
 
// Объекты
const user: {
  name: string;
  age: number;
  email?: string; // необязательное поле
} = {
  name: "Иван",
  age: 25,
};
 
// Массив объектов
const users: Array<{ name: string; age: number }> = [
  { name: "Иван", age: 25 },
  { name: "Анна", age: 30 },
];

Any, Unknown и Never

// any — отключает проверку типов (используй только в крайнем случае)
let anything: any = 42;
anything = "строка"; // OK
anything = {}; // OK
 
// unknown — безопасная альтернатива any
let userInput: unknown;
userInput = 5;
userInput = "строка";
 
// Нужна проверка типа перед использованием
if (typeof userInput === "string") {
  console.log(userInput.toUpperCase()); // OK
}
 
// never — тип, который никогда не наступает
function throwError(message: string): never {
  throw new Error(message);
}
 
function infiniteLoop(): never {
  while (true) {}
}

Избегай any — это выход из системы типов. Используй unknown, если тип действительно неизвестен, и проверяй его перед использованием.

Union и Literal Types

// Union — значение может быть одним из нескольких типов
type Status = "pending" | "success" | "error";
type ID = string | number;
 
function printId(id: ID) {
  console.log(`ID: ${id}`);
}
 
printId(101); // OK
printId("abc123"); // OK
printId(true); // Error
 
// Literal types — конкретные значения как типы
type Direction = "up" | "down" | "left" | "right";
 
function move(direction: Direction) {
  console.log(`Moving ${direction}`);
}
 
move("up"); // OK
move("diagonal"); // Error

Функции

Типизация параметров и возвращаемых значений

// JavaScript
function add(a, b) {
  return a + b;
}
 
// TypeScript
function add(a: number, b: number): number {
  return a + b;
}
 
// Arrow function
const multiply = (a: number, b: number): number => a * b;
 
// Необязательные параметры
function greet(name: string, greeting?: string): string {
  return `${greeting || "Привет"}, ${name}!`;
}
 
// Параметры по умолчанию
function createUser(name: string, role: string = "user") {
  return { name, role };
}
 
// Rest параметры
function sum(...numbers: number[]): number {
  return numbers.reduce((acc, n) => acc + n, 0);
}

Перегрузка функций

// Объявляем несколько сигнатур
function format(value: string): string;
function format(value: number): string;
function format(value: boolean): string;
 
// Реализация
function format(value: string | number | boolean): string {
  if (typeof value === "string") return value.toUpperCase();
  if (typeof value === "number") return value.toFixed(2);
  return value ? "true" : "false";
}
 
console.log(format("hello")); // "HELLO"
console.log(format(42)); // "42.00"
console.log(format(true)); // "true"

Функции как типы

// Тип функции
type MathOperation = (a: number, b: number) => number;
 
const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;
 
// Функции высшего порядка
function applyOperation(
  a: number,
  b: number,
  operation: MathOperation
): number {
  return operation(a, b);
}
 
console.log(applyOperation(5, 3, add)); // 8
console.log(applyOperation(5, 3, subtract)); // 2

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

Интерфейсы и Type Aliases

Interface vs Type

// Interface — для описания структуры объектов
interface User {
  id: number;
  name: string;
  email: string;
  age?: number; // необязательное поле
  readonly createdAt: Date; // только для чтения
}
 
// Type Alias — более универсальный
type UserType = {
  id: number;
  name: string;
  email: string;
};
 
// Type может описывать не только объекты
type ID = string | number;
type Status = "active" | "inactive";
type Callback = (data: string) => void;

Когда использовать что? Interface для объектов и когда планируется расширение. Type для union, tuple, примитивов и утилитных типов.

Расширение интерфейсов

// Наследование интерфейсов
interface Animal {
  name: string;
  age: number;
}
 
interface Dog extends Animal {
  breed: string;
  bark(): void;
}
 
const myDog: Dog = {
  name: "Бобик",
  age: 3,
  breed: "Лабрадор",
  bark() {
    console.log("Гав!");
  },
};
 
// Множественное наследование
interface Swimmer {
  swim(): void;
}
 
interface Flyer {
  fly(): void;
}
 
interface Duck extends Animal, Swimmer, Flyer {
  quack(): void;
}

Declaration Merging

// Interface можно дополнять (только interface!)
interface Window {
  customProperty: string;
}
 
interface Window {
  anotherProperty: number;
}
 
// Теперь Window имеет оба свойства
window.customProperty = "test";
window.anotherProperty = 42;

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

// Объект с динамическими ключами
interface StringMap {
  [key: string]: string;
}
 
const translations: StringMap = {
  hello: "Привет",
  goodbye: "Пока",
  thanks: "Спасибо",
};
 
// Комбинация с фиксированными свойствами
interface Config {
  apiUrl: string;
  timeout: number;
  [key: string]: string | number; // динамические поля
}
 
const config: Config = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  debug: true, // дополнительное поле
  retries: 3, // ещё одно
};

Generics (Обобщённые типы)

Зачем нужны Generics

// Без Generics — нужна перегрузка или any
function identity(value: any): any {
  return value;
}
 
// С Generics — тип сохраняется
function identity<T>(value: T): T {
  return value;
}
 
const num = identity(42); // тип number
const str = identity("hello"); // тип string
const obj = identity({ name: "Иван" }); // тип { name: string }

Generics — это "параметры типов". Как переменные в функциях, только для типов. Пишешь один раз, используешь с любыми типами.

Generics в функциях

// Функция с generic типом
function getFirstElement<T>(array: T[]): T | undefined {
  return array[0];
}
 
const firstNumber = getFirstElement([1, 2, 3]); // number | undefined
const firstName = getFirstElement(["Анна", "Борис"]); // string | undefined
 
// Несколько generic параметров
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}
 
const nameAge = pair("Иван", 25); // [string, number]
const coordinates = pair(55.75, 37.61); // [number, number]
 
// Generic с ограничениями (constraints)
interface HasLength {
  length: number;
}
 
function logLength<T extends HasLength>(item: T): void {
  console.log(item.length);
}
 
logLength("hello"); // OK, у string есть length
logLength([1, 2, 3]); // OK, у array есть length
logLength(42); // Error, у number нет length

Generics в интерфейсах и типах

// Generic интерфейс
interface Result<T> {
  success: boolean;
  data: T;
  error?: string;
}
 
const userResult: Result<User> = {
  success: true,
  data: {
    id: 1,
    name: "Иван",
    email: "ivan@example.com",
    createdAt: new Date(),
  },
};
 
const numberResult: Result<number> = {
  success: false,
  error: "Не удалось получить число",
  data: 0,
};
 
// Generic type alias
type ApiResponse<T> = {
  status: number;
  data: T;
  timestamp: Date;
};
 
// Generic с default типом
interface Container<T = string> {
  value: T;
}
 
const stringContainer: Container = { value: "hello" }; // использует default string
const numberContainer: Container<number> = { value: 42 };

Generics в классах

class Box<T> {
  private value: T;
 
  constructor(value: T) {
    this.value = value;
  }
 
  getValue(): T {
    return this.value;
  }
 
  setValue(value: T): void {
    this.value = value;
  }
}
 
const stringBox = new Box("hello");
console.log(stringBox.getValue()); // "hello"
 
const numberBox = new Box(42);
console.log(numberBox.getValue()); // 42

Pro tip: Generics особенно полезны для создания переиспользуемых компонентов и утилит. Вместо написания отдельной функции для каждого типа, создай одну generic функцию.

// Generic класс с ограничениями
class Repository<T extends { id: number }> {
  private items: T[] = [];
 
  add(item: T): void {
    this.items.push(item);
  }
 
  findById(id: number): T | undefined {
    return this.items.find((item) => item.id === id);
  }
 
  getAll(): T[] {
    return [...this.items];
  }
}
 
interface Product {
  id: number;
  name: string;
  price: number;
}
 
const productRepo = new Repository<Product>();
productRepo.add({ id: 1, name: "Laptop", price: 1000 });

Продвинутые типы (Advanced Types)

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

Mapped Types

Mapped Types позволяют создавать новые типы на основе существующих, трансформируя каждое свойство.

// Базовый Mapped Type
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};
 
type Partial<T> = {
  [P in keyof T]?: T[P];
};
 
// Пример использования
interface User {
  id: number;
  name: string;
  email: string;
}
 
type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string; readonly email: string }
 
// Создание собственных Mapped Types
type Nullable<T> = {
  [P in keyof T]: T[P] | null;
};
 
type NullableUser = Nullable<User>;
// { id: number | null; name: string | null; email: string | null }
 
// Mapped Type с трансформацией ключей
type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
 
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; getEmail: () => string }

Mapped Types — это фундамент для создания собственных утилитных типов. Все встроенные utility types (Partial, Readonly, Pick) реализованы через Mapped Types.

Conditional Types

Conditional Types позволяют создавать типы на основе условий (похоже на тернарный оператор для типов).

// Базовый синтаксис: T extends U ? X : Y
type IsString<T> = T extends string ? true : false;
 
type A = IsString<string>; // true
type B = IsString<number>; // false
 
// Практический пример: Extract типа из Promise
type Awaited<T> = T extends Promise<infer U> ? U : T;
 
type PromiseNumber = Promise<number>;
type Number = Awaited<PromiseNumber>; // number
 
type NotPromise = Awaited<string>; // string
 
// Рекурсивный Awaited для вложенных Promise
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
 
type Nested = Promise<Promise<Promise<number>>>;
type Result = DeepAwaited<Nested>; // number
 
// Conditional Type с несколькими условиями
type TypeName<T> = T extends string
  ? "string"
  : T extends number
    ? "number"
    : T extends boolean
      ? "boolean"
      : T extends Function
        ? "function"
        : "object";
 
type T1 = TypeName<string>; // "string"
type T2 = TypeName<42>; // "number"
type T3 = TypeName<() => void>; // "function"

Infer Keyword

infer позволяет извлекать типы из других типов внутри conditional types.

// Извлечение типа возвращаемого значения
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
 
function getUser() {
  return { id: 1, name: "Иван" };
}
 
type User = ReturnType<typeof getUser>;
// { id: number; name: string }
 
// Извлечение типа параметров
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
 
function createPost(title: string, content: string, tags: string[]) {
  return { title, content, tags };
}
 
type PostParams = Parameters<typeof createPost>;
// [title: string, content: string, tags: string[]]
 
// Извлечение типа элемента массива
type ArrayElement<T> = T extends (infer E)[] ? E : never;
 
type Numbers = number[];
type Num = ArrayElement<Numbers>; // number
 
// Практический пример: извлечение props из компонента
type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;
 
function MyButton(props: { label: string; onClick: () => void }) {
  return <button onClick={props.onClick}>{props.label}</button>;
}
 
type ButtonProps = ComponentProps<typeof MyButton>;
// { label: string; onClick: () => void }

infer работает только внутри conditional types. Это мощный инструмент для извлечения типов из сложных конструкций.

Template Literal Types

Template Literal Types позволяют манипулировать строковыми типами (literal types) как шаблонными строками.

// Базовый синтаксис
type World = "world";
type Greeting = `hello ${World}`; // "hello world"
 
// Комбинирование нескольких типов
type EmailLocale = "ru" | "en";
type EmailType = "welcome" | "reset";
type EmailTemplate = `${EmailType}_${EmailLocale}`;
// "welcome_ru" | "welcome_en" | "reset_ru" | "reset_en"
 
// Практический пример: CSS классы
type Color = "primary" | "secondary" | "danger";
type Size = "sm" | "md" | "lg";
type ButtonClass = `btn-${Color}-${Size}`;
// "btn-primary-sm" | "btn-primary-md" | ... (9 вариантов)
 
// Event handlers
type EventName = "click" | "focus" | "blur";
type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
 
// Типобезопасный builder pattern
type BuilderMethod<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (
    value: T[K]
  ) => BuilderMethod<T>;
};
 
interface Config {
  host: string;
  port: number;
  debug: boolean;
}
 
type ConfigBuilder = BuilderMethod<Config>;
// {
//   setHost: (value: string) => ConfigBuilder;
//   setPort: (value: number) => ConfigBuilder;
//   setDebug: (value: boolean) => ConfigBuilder;
// }
 
// Реальный пример: типизация API эндпоинтов
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIPath = "/users" | "/posts" | "/comments";
type APIEndpoint = `${HTTPMethod} ${APIPath}`;
// "GET /users" | "POST /users" | ... (12 вариантов)
 
const endpoints: Record<APIEndpoint, Function> = {
  "GET /users": () => {},
  "POST /users": () => {},
  // TypeScript проверит, что все эндпоинты описаны
};

Intrinsic String Manipulation Types

TypeScript предоставляет встроенные типы для манипуляции строками.

// Uppercase - преобразует в верхний регистр
type UpperCaseGreeting = Uppercase<"hello">; // "HELLO"
 
// Lowercase - преобразует в нижний регистр
type LowerCaseGreeting = Lowercase<"HELLO">; // "hello"
 
// Capitalize - делает первую букву заглавной
type CapitalizedGreeting = Capitalize<"hello">; // "Hello"
 
// Uncapitalize - делает первую букву строчной
type UncapitalizedGreeting = Uncapitalize<"Hello">; // "hello"
 
// Практический пример: генерация методов
type HTTPMethod = "get" | "post" | "put" | "delete";
type FetchMethod = `fetch${Capitalize<HTTPMethod>}`;
// "fetchGet" | "fetchPost" | "fetchPut" | "fetchDelete"
 
interface APIClient {
  fetchGet: (url: string) => Promise<any>;
  fetchPost: (url: string, data: any) => Promise<any>;
  fetchPut: (url: string, data: any) => Promise<any>;
  fetchDelete: (url: string) => Promise<any>;
}

Recursive Types

Рекурсивные типы позволяют описывать структуры с бесконечной вложенностью.

// JSON тип
type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };
 
const data: JSONValue = {
  name: "Иван",
  age: 25,
  hobbies: ["coding", "reading"],
  address: {
    city: "Москва",
    coords: {
      lat: 55.75,
      lng: 37.61,
    },
  },
};
 
// Deep Partial - делает все вложенные поля optional
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
 
interface NestedConfig {
  database: {
    host: string;
    port: number;
    credentials: {
      user: string;
      password: string;
    };
  };
}
 
type PartialConfig = DeepPartial<NestedConfig>;
// Все поля на всех уровнях станут optional
 
// Deep Readonly - делает все вложенные поля readonly
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
 
// Flatten - разворачивает вложенные массивы
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;
 
type Nested = number[][][];
type Flat = Flatten<Nested>; // number

Продвинутые типы — это мета-программирование на уровне типов. Они позволяют создавать типобезопасные API и библиотеки, где ошибки находятся на этапе компиляции.

Практическое применение

// Типобезопасный EventEmitter
type EventMap = {
  click: { x: number; y: number };
  keypress: { key: string };
  scroll: { top: number };
};
 
class TypedEventEmitter<T extends Record<string, any>> {
  private listeners: {
    [K in keyof T]?: Array<(data: T[K]) => void>;
  } = {};
 
  on<K extends keyof T>(event: K, callback: (data: T[K]) => void) {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(callback);
  }
 
  emit<K extends keyof T>(event: K, data: T[K]) {
    this.listeners[event]?.forEach((cb) => cb(data));
  }
}
 
const emitter = new TypedEventEmitter<EventMap>();
 
emitter.on("click", (data) => {
  console.log(data.x, data.y); // TypeScript знает структуру
});
 
emitter.emit("click", { x: 10, y: 20 }); // OK
emitter.emit("click", { x: 10 }); // Error: missing 'y'
 
// Типобезопасный путь к вложенным свойствам
type PathsToProps<T, Prefix extends string = ""> = {
  [K in keyof T]: T[K] extends object
    ? PathsToProps<T[K], `${Prefix}${string & K}.`> | `${Prefix}${string & K}`
    : `${Prefix}${string & K}`;
}[keyof T];
 
interface User {
  name: string;
  address: {
    city: string;
    coords: {
      lat: number;
      lng: number;
    };
  };
}
 
type UserPaths = PathsToProps<User>;
// "name" | "address" | "address.city" | "address.coords" | "address.coords.lat" | "address.coords.lng"
 
function get<T, P extends PathsToProps<T>>(obj: T, path: P): any {
  // Реализация
}
 
get(user, "address.coords.lat"); // OK
get(user, "address.invalid"); // Error

Утилитные типы (Utility Types)

TypeScript предоставляет встроенные типы для трансформации других типов.

Partial и Required

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}
 
// Partial — все поля становятся необязательными
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; age?: number }
 
function updateUser(id: number, updates: Partial<User>) {
  // можем обновить только некоторые поля
}
 
updateUser(1, { name: "Новое имя" }); // OK
updateUser(1, { email: "new@email.com", age: 26 }); // OK
 
// Required — все поля становятся обязательными
interface OptionalUser {
  id?: number;
  name?: string;
}
 
type RequiredUser = Required<OptionalUser>;
// { id: number; name: string }

Pick и Omit

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}
 
// Pick — выбрать только указанные поля
type UserPreview = Pick<User, "id" | "name" | "email">;
// { id: number; name: string; email: string }
 
// Omit — исключить указанные поля
type UserWithoutPassword = Omit<User, "password">;
// { id: number; name: string; email: string; createdAt: Date }
 
type PublicUser = Omit<User, "password" | "email">;
// { id: number; name: string; createdAt: Date }

Pick и Omit — самые используемые утилитные типы. Позволяют создавать новые типы на основе существующих без дублирования кода.

Record и Readonly

// Record — создать объект с ключами определённого типа
type UserRole = "admin" | "user" | "guest";
 
const permissions: Record<UserRole, string[]> = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"],
  guest: ["read"],
};
 
// Readonly — все поля становятся readonly
type ReadonlyUser = Readonly<User>;
 
const user: ReadonlyUser = {
  id: 1,
  name: "Иван",
  email: "ivan@example.com",
  password: "secret",
  createdAt: new Date(),
};
 
user.name = "Петр"; // Error: Cannot assign to 'name' because it is a read-only property
 
// Readonly для массивов
const numbers: ReadonlyArray<number> = [1, 2, 3];
numbers.push(4); // Error

ReturnType и Parameters

// ReturnType — получить тип возвращаемого значения функции
function createUser(name: string, age: number) {
  return {
    id: Math.random(),
    name,
    age,
    createdAt: new Date(),
  };
}
 
type User = ReturnType<typeof createUser>;
// { id: number; name: string; age: number; createdAt: Date }
 
// Parameters — получить типы параметров функции
type CreateUserParams = Parameters<typeof createUser>;
// [name: string, age: number]
 
function callCreateUser(params: CreateUserParams) {
  return createUser(...params);
}

Exclude, Extract и NonNullable

// Exclude — исключить типы из union
type AllTypes = string | number | boolean | null;
type WithoutNull = Exclude<AllTypes, null>;
// string | number | boolean
 
type Status = "pending" | "success" | "error" | "cancelled";
type ActiveStatus = Exclude<Status, "cancelled">;
// "pending" | "success" | "error"
 
// Extract — извлечь только указанные типы
type NumberOrString = Extract<AllTypes, string | number>;
// string | number
 
// NonNullable — исключить null и undefined
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// string

Интеграция с React

Компоненты и Props

import { ReactNode } from "react";
 
// Функциональный компонент с типизированными props
interface ButtonProps {
  children: ReactNode;
  onClick: () => void;
  variant?: "primary" | "secondary";
  disabled?: boolean;
}
 
function Button({ children, onClick, variant = "primary", disabled }: ButtonProps) {
  return (
    <button
      onClick={onClick}
      className={`btn btn-${variant}`}
      disabled={disabled}
    >
      {children}
    </button>
  );
}
 
// Использование
<Button onClick={() => console.log("Clicked")}>Нажми меня</Button>;
 
// Generic компонент
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => ReactNode;
}
 
function List<T>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}
 
// Использование
interface User {
  id: number;
  name: string;
}
 
const users: User[] = [
  { id: 1, name: "Иван" },
  { id: 2, name: "Мария" },
];
 
<List items={users} renderItem={(user) => <span>{user.name}</span>} />;

Hooks с TypeScript

import { useState, useEffect, useRef } from "react";
 
// useState с явным типом
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
 
// useState с type inference (рекомендуется, если очевидно)
const [count, setCount] = useState(0); // number
const [name, setName] = useState(""); // string
 
// useEffect
useEffect(() => {
  const fetchUser = async () => {
    const response = await fetch("/api/user");
    const data: User = await response.json();
    setUser(data);
  };
 
  fetchUser();
}, []);
 
// useRef
const inputRef = useRef<HTMLInputElement>(null);
 
function focusInput() {
  inputRef.current?.focus();
}
 
// Custom hook
interface UseCounterReturn {
  count: number;
  increment: () => void;
  decrement: () => void;
  reset: () => void;
}
 
function useCounter(initialValue: number = 0): UseCounterReturn {
  const [count, setCount] = useState(initialValue);
 
  const increment = () => setCount((c) => c + 1);
  const decrement = () => setCount((c) => c - 1);
  const reset = () => setCount(initialValue);
 
  return { count, increment, decrement, reset };
}

Для React-компонентов типизация props — обязательна. useState обычно выводит тип автоматически, явное указание нужно только для сложных случаев.

Распространенная ошибка: Не забывай типизировать useState, когда initial value — null или undefined. Без явного типа TypeScript не сможет вывести правильный union type.

// ❌ Плохо
const [user, setUser] = useState(null); // тип: null
 
// ✅ Хорошо
const [user, setUser] = useState<User | null>(null); // тип: User | null

Event Handlers

import { ChangeEvent, FormEvent, MouseEvent } from "react";
 
function Form() {
  // Типизация event handlers
  const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };
 
  const handleTextareaChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
    console.log(e.target.value);
  };
 
  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // обработка формы
  };
 
  const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
    console.log(e.clientX, e.clientY);
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input type="text" onChange={handleInputChange} />
      <textarea onChange={handleTextareaChange} />
      <button onClick={handleClick}>Отправить</button>
    </form>
  );
}

Context API

import { createContext, useContext, ReactNode } from "react";
 
interface User {
  id: number;
  name: string;
  email: string;
}
 
interface AuthContextType {
  user: User | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  isAuthenticated: boolean;
}
 
// Создание контекста с типом
const AuthContext = createContext<AuthContextType | undefined>(undefined);
 
// Provider
interface AuthProviderProps {
  children: ReactNode;
}
 
function AuthProvider({ children }: AuthProviderProps) {
  const [user, setUser] = useState<User | null>(null);
 
  const login = async (email: string, password: string) => {
    // логика входа
    const userData = await fetch("/api/login", {
      method: "POST",
      body: JSON.stringify({ email, password }),
    }).then((r) => r.json());
    setUser(userData);
  };
 
  const logout = () => {
    setUser(null);
  };
 
  const value: AuthContextType = {
    user,
    login,
    logout,
    isAuthenticated: !!user,
  };
 
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
 
// Custom hook для использования контекста
function useAuth(): AuthContextType {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error("useAuth must be used within AuthProvider");
  }
  return context;
}

Интеграция с Next.js

Page и Layout типы

// app/page.tsx - Next.js 13+ App Router
interface HomePageProps {
  searchParams: { [key: string]: string | string[] | undefined };
}
 
export default function HomePage({ searchParams }: HomePageProps) {
  return <div>Home Page</div>;
}
 
// app/blog/[slug]/page.tsx - динамический роут
interface BlogPostPageProps {
  params: { slug: string };
  searchParams: { [key: string]: string | string[] | undefined };
}
 
export default function BlogPostPage({ params, searchParams }: BlogPostPageProps) {
  return <div>Post: {params.slug}</div>;
}
 
// Layout
import { ReactNode } from "react";
 
interface RootLayoutProps {
  children: ReactNode;
}
 
export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <html lang="ru">
      <body>{children}</body>
    </html>
  );
}

API Routes

// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
 
interface User {
  id: number;
  name: string;
  email: string;
}
 
// GET handler
export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const query = searchParams.get("query");
 
  const users: User[] = await fetchUsers(query);
 
  return NextResponse.json(users);
}
 
// POST handler
interface CreateUserBody {
  name: string;
  email: string;
}
 
export async function POST(request: NextRequest) {
  const body: CreateUserBody = await request.json();
 
  const newUser: User = await createUser(body);
 
  return NextResponse.json(newUser, { status: 201 });
}
 
// app/api/users/[id]/route.ts - динамический роут
interface RouteContext {
  params: { id: string };
}
 
export async function GET(request: NextRequest, context: RouteContext) {
  const userId = parseInt(context.params.id);
  const user = await fetchUserById(userId);
 
  if (!user) {
    return NextResponse.json({ error: "User not found" }, { status: 404 });
  }
 
  return NextResponse.json(user);
}

Server Components и Client Components

// Server Component (по умолчанию в App Router)
// app/users/page.tsx
async function UsersPage() {
  // Можем делать async операции прямо в компоненте
  const users = await fetchUsers();
 
  return (
    <div>
      {users.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}
 
// Client Component
// components/counter.tsx
"use client";
 
import { useState } from "react";
 
interface CounterProps {
  initialValue?: number;
}
 
export function Counter({ initialValue = 0 }: CounterProps) {
  const [count, setCount] = useState(initialValue);
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Metadata API

// Статический metadata
import { Metadata } from "next";
 
export const metadata: Metadata = {
  title: "Мой сайт",
  description: "Описание сайта",
  openGraph: {
    title: "Мой сайт",
    description: "Описание сайта",
    images: ["/og-image.jpg"],
  },
};
 
// Динамический metadata
interface GenerateMetadataProps {
  params: { slug: string };
  searchParams: { [key: string]: string | string[] | undefined };
}
 
export async function generateMetadata({
  params,
}: GenerateMetadataProps): Promise<Metadata> {
  const post = await fetchPost(params.slug);
 
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  };
}

Next.js 13+ App Router предоставляет готовые типы для большинства случаев: NextRequest, NextResponse, Metadata, типы для params и searchParams.

Настройка TypeScript

tsconfig.json

{
  "compilerOptions": {
    // Версия JavaScript на выходе
    "target": "ES2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "jsx": "preserve",
 
    // Модульная система
    "module": "ESNext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
 
    // Строгость
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
 
    // Дополнительные проверки
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
 
    // Path aliases
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@/components/*": ["./src/components/*"],
      "@/utils/*": ["./src/utils/*"]
    },
 
    // Incremental compilation
    "incremental": true,
 
    // Next.js specific
    "plugins": [{ "name": "next" }]
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

Начни со строгих настроек ("strict": true). Это может быть сложно на старте, но избавит от багов в будущем. Если совсем тяжело — отключай проверки по одной.

Pro tip: Используй skipLibCheck: true для ускорения компиляции. Это пропускает проверку типов в node_modules, что может сократить время сборки на 50-70% в больших проектах.

Полезные правила для начала

{
  "compilerOptions": {
    // Обязательно
    "strict": true, // включает все строгие проверки
    "noImplicitAny": true, // запрещает неявный any
 
    // Рекомендуется
    "strictNullChecks": true, // null и undefined - отдельные типы
    "noUnusedLocals": true, // предупреждает о неиспользуемых переменных
    "noUnusedParameters": true, // предупреждает о неиспользуемых параметрах
 
    // Для комфорта
    "skipLibCheck": true, // не проверять типы в node_modules
    "esModuleInterop": true // совместимость с CommonJS модулями
  }
}

Практические советы

Миграция с JavaScript

// 1. Переименуй .js файлы в .ts
// 2. Добавь // @ts-check в начало JS-файлов для постепенной миграции
// 3. Используй "allowJs": true в tsconfig.json
 
// Было (JS)
function calculateTotal(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}
 
// Стало (TS) - минимальная типизация
function calculateTotal(items: any[]) {
  return items.reduce((sum, item) => sum + item.price, 0);
}
 
// Идеально (TS) - полная типизация
interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}
 
function calculateTotal(items: CartItem[]): number {
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}

Чек-лист миграции проекта на TypeScript

Подготовка (День 1)

1. Установка и настройка

# Установить TypeScript и типы
npm install -D typescript @types/node @types/react @types/react-dom
 
# Создать tsconfig.json
npx tsc --init
 
# Установить типы для используемых библиотек
npm install -D @types/lodash @types/jest

2. Настроить tsconfig.json для постепенной миграции

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM"],
    "jsx": "react-jsx",
 
    // Разрешить JS файлы во время миграции
    "allowJs": true,
    "checkJs": false,
 
    // Начать с минимальной строгости
    "strict": false,
    "noImplicitAny": false,
    "strictNullChecks": false,
 
    // Incrementally increase strictness
    "noUnusedLocals": false,
    "noUnusedParameters": false,
 
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "build", "dist"]
}

3. Обновить package.json скрипты

{
  "scripts": {
    "type-check": "tsc --noEmit",
    "type-check:watch": "tsc --noEmit --watch",
    "build": "tsc && vite build"
  }
}

Этап 1: Базовая миграция (Неделя 1-2)

Приоритизация файлов для миграции:

  1. ✅ Утилиты и хелперы (src/utils/, src/helpers/)
  2. ✅ Константы и конфигурация (src/constants/, src/config/)
  3. ✅ Типы и интерфейсы (src/types/)
  4. ✅ Модели данных (src/models/)
  5. ⏳ Сервисы и API клиенты (src/services/, src/api/)
  6. ⏳ UI компоненты (начни с простых)
  7. ⏳ Бизнес-логика и store
  8. ⏳ Страницы и роуты

Пошаговый процесс:

// Шаг 1: Переименуй файл .js → .ts (или .jsx → .tsx для React)
// utils/format.js → utils/format.ts
 
// Шаг 2: Добавь минимальные типы (можно начать с any)
function formatPrice(price: any): string {
  return `$${price.toFixed(2)}`;
}
 
// Шаг 3: Постепенно улучшай типы
function formatPrice(price: number): string {
  return `$${price.toFixed(2)}`;
}
 
// Шаг 4: Добавь error handling
function formatPrice(price: number | null | undefined): string {
  if (price == null) return "$0.00";
  return `$${price.toFixed(2)}`;
}

Этап 2: Увеличение строгости (Неделя 3-4)

Постепенно включай строгие правила:

{
  "compilerOptions": {
    // Этап 2.1: Включи базовую строгость
    "noImplicitAny": true,
 
    // Этап 2.2: Null checks
    "strictNullChecks": true,
 
    // Этап 2.3: Полная строгость
    "strict": true,
 
    // Этап 2.4: Дополнительные проверки
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

После каждого изменения:

# Проверь ошибки
npm run type-check
 
# Исправь ошибки по одной
# Commit изменения
git add .
git commit -m "chore: enable strictNullChecks"

Этап 3: Рефакторинг (Неделя 5+)

Замени any на конкретные типы:

// ❌ До
function processData(data: any) {
  return data.map((item: any) => item.value);
}
 
// ✅ После
interface DataItem {
  value: string;
  id: number;
}
 
function processData(data: DataItem[]): string[] {
  return data.map((item) => item.value);
}

Создай общие типы:

// src/types/api.ts
export interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}
 
export interface User {
  id: number;
  name: string;
  email: string;
}
 
// Используй в коде
import { ApiResponse, User } from "@/types/api";
 
async function getUser(id: number): Promise<ApiResponse<User>> {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

Метрики успешной миграции

Отслеживай прогресс:

# Количество TS файлов vs JS файлов
find src -name "*.ts" -o -name "*.tsx" | wc -l
find src -name "*.js" -o -name "*.jsx" | wc -l
 
# Количество any в кодовой базе (стремись к минимуму)
grep -r "any" src --include="*.ts" --include="*.tsx" | wc -l

Чек-лист готовности:

  • 100% файлов мигрировано (.ts/.tsx)
  • strict: true в tsconfig.json
  • 0 ошибок компиляции
  • < 5% использования any в коде
  • Все API ответы типизированы
  • Все props компонентов типизированы
  • CI/CD запускает type-check
  • Команда обучена TypeScript

Частые проблемы при миграции

Проблема 1: Библиотеки без типов

// Решение: создай declaration файл
// types/my-library.d.ts
declare module "my-library" {
  export function doSomething(value: string): number;
}
 
// Или используй any временно
declare module "legacy-library";

Проблема 2: Динамические данные

// ❌ Плохо
const data: any = await fetchData();
 
// ✅ Хорошо - используй валидацию
import { z } from "zod";
 
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
});
 
const data = await fetchData();
const user = UserSchema.parse(data); // Проверка в runtime

Проблема 3: Legacy код со сложной логикой

// Стратегия: изолируй в wrapper
// legacy/old-code.js - оставь как есть
export function complexLegacyFunction(data) {
  // сложная логика
}
 
// wrapper.ts - типизируй интерфейс
import { complexLegacyFunction } from "./legacy/old-code";
 
interface Input {
  value: string;
}
 
interface Output {
  result: number;
}
 
export function typedWrapper(input: Input): Output {
  const result = complexLegacyFunction(input);
  return result as Output;
}

Миграция — это марафон, а не спринт. Начни с малого, мигрируй постепенно, увеличивай строгость по мере готовности команды. Даже частичная типизация даёт пользу.

Работа с внешними библиотеками

// Установка типов для библиотек
// npm install -D @types/lodash
// npm install -D @types/react
// npm install -D @types/node
 
import _ from "lodash";
 
// Если типов нет, можно создать declaration файл
// types/my-library.d.ts
declare module "my-library" {
  export function doSomething(value: string): number;
}
 
// Или использовать any для быстрого решения
declare module "untyped-library";

Частые ошибки и их решение

// Ошибка: Object is possibly 'null'
const element = document.getElementById("app");
element.innerHTML = "Hello"; // Error
 
// Решение 1: Non-null assertion (!)
element!.innerHTML = "Hello";
 
// Решение 2: Optional chaining
element?.setAttribute("class", "active");
 
// Решение 3: Проверка
if (element) {
  element.innerHTML = "Hello";
}
 
// Ошибка: Type 'string | undefined' is not assignable to type 'string'
function greet(name?: string) {
  const greeting: string = name; // Error
}
 
// Решение 1: Default value
function greet(name?: string) {
  const greeting: string = name || "Гость";
}
 
// Решение 2: Non-null assertion
function greet(name?: string) {
  const greeting: string = name!; // Утверждаем, что name не undefined
}
 
// Ошибка: Argument of type 'number' is not assignable to parameter of type 'string'
function printId(id: string) {
  console.log(id);
}
 
printId(123); // Error
 
// Решение: Union type
function printId(id: string | number) {
  console.log(id);
}

Избегай ! (non-null assertion) и as (type assertion) без крайней необходимости. Это способы "обмануть" компилятор. Лучше использовать проверки типов.

Type Guards

// Type guard функции
function isString(value: unknown): value is string {
  return typeof value === "string";
}
 
function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "name" in value
  );
}
 
// Использование
function processValue(value: string | number) {
  if (isString(value)) {
    console.log(value.toUpperCase()); // TypeScript знает, что это string
  } else {
    console.log(value.toFixed(2)); // TypeScript знает, что это number
  }
}
 
// Discriminated Unions
type Success = {
  type: "success";
  data: User;
};
 
type Error = {
  type: "error";
  message: string;
};
 
type Result = Success | Error;
 
function handleResult(result: Result) {
  if (result.type === "success") {
    console.log(result.data); // TypeScript знает, что есть data
  } else {
    console.log(result.message); // TypeScript знает, что есть message
  }
}

Типичные ошибки и антипаттерны

Изучение того, чего НЕ нужно делать, так же важно, как и изучение правильных подходов.

❌ Антипаттерн: Злоупотребление any

// ❌ Плохо — отключает всю проверку типов
function processData(data: any) {
  return data.map((item: any) => item.value);
}
 
// ✅ Хорошо — используй конкретные типы
interface DataItem {
  value: string;
}
 
function processData(data: DataItem[]) {
  return data.map((item) => item.value);
}
 
// ✅ Если тип действительно неизвестен — используй unknown
function processUnknownData(data: unknown) {
  if (Array.isArray(data)) {
    return data.map((item) => {
      if (typeof item === "object" && item !== null && "value" in item) {
        return item.value;
      }
    });
  }
}

any — это "аварийный выход" из системы типов. Используй только когда действительно нет другого выбора (например, при работе с устаревшими библиотеками без типов).

❌ Антипаттерн: Избыточный type assertion

// ❌ Плохо — заставляем компилятор поверить нам
const user = getUserData() as User;
const id = user.id as number;
 
// ✅ Хорошо — проверяем типы
function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    typeof value.id === "number"
  );
}
 
const data = getUserData();
if (isUser(data)) {
  const id = data.id; // TypeScript знает, что это number
}
 
// ✅ Или используй валидацию (zod, yup, io-ts)
import { z } from "zod";
 
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});
 
const data = getUserData();
const user = UserSchema.parse(data); // выбросит ошибку, если не соответствует

❌ Антипаттерн: Дублирование типов и интерфейсов

// ❌ Плохо — дублирование данных
interface User {
  id: number;
  name: string;
  email: string;
}
 
interface UserDTO {
  id: number;
  name: string;
  email: string;
}
 
// ✅ Хорошо — используй утилитные типы
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}
 
type UserDTO = Omit<User, "password">;
 
// ✅ Или выводи типы из функций
function createUser(name: string, email: string) {
  return { id: Math.random(), name, email, createdAt: new Date() };
}
 
type User = ReturnType<typeof createUser>;

❌ Антипаттерн: Игнорирование null и undefined

// ❌ Плохо — предполагаем, что значение всегда есть
function greet(name: string | undefined) {
  return `Hello, ${name.toUpperCase()}`; // Error в runtime!
}
 
// ✅ Хорошо — проверяем на null/undefined
function greet(name: string | undefined) {
  return `Hello, ${name?.toUpperCase() ?? "Guest"}`;
}
 
// ✅ Или используй default параметры
function greet(name: string = "Guest") {
  return `Hello, ${name.toUpperCase()}`;
}
 
// ✅ Или используй non-null assertion только когда ТОЧНО знаешь
function greet(name: string | undefined) {
  // Если уверены, что name точно есть (например, после проверки)
  return `Hello, ${name!.toUpperCase()}`;
}

Включай strictNullChecks в tsconfig.json — это заставит явно обрабатывать null и undefined, предотвращая runtime ошибки.

❌ Антипаттерн: Избыточная типизация очевидных вещей

// ❌ Плохо — избыточные аннотации
const name: string = "Иван";
const age: number = 25;
const isActive: boolean = true;
 
const numbers: Array<number> = [1, 2, 3].map((n: number): number => n * 2);
 
// ✅ Хорошо — type inference
const name = "Иван"; // TypeScript знает, что это string
const age = 25; // number
const isActive = true; // boolean
 
const numbers = [1, 2, 3].map((n) => n * 2); // number[]
 
// ❌ Плохо — дублирование типа в возврате
const getUser = (): { id: number; name: string } => {
  return { id: 1, name: "Иван" };
};
 
// ✅ Хорошо — type inference из return
const getUser = () => {
  return { id: 1, name: "Иван" };
}; // TypeScript выводит тип сам

❌ Антипаттерн: Неправильная работа с Union типами

// ❌ Плохо — проверяем через type assertion
type Response =
  | { success: true; data: User }
  | { success: false; error: string };
 
function handleResponse(response: Response) {
  if ((response as any).data) {
    console.log((response as any).data);
  }
}
 
// ✅ Хорошо — используй discriminated unions
type Response =
  | { success: true; data: User }
  | { success: false; error: string };
 
function handleResponse(response: Response) {
  if (response.success) {
    console.log(response.data); // TypeScript знает, что это first branch
  } else {
    console.log(response.error); // TypeScript знает, что это second branch
  }
}

❌ Антипаттерн: Пустые интерфейсы

// ❌ Плохо — пустой интерфейс бесполезен
interface Props {}
 
function MyComponent(props: Props) {
  return <div>Hello</div>;
}
 
// ✅ Хорошо — не типизируй, если нет props
function MyComponent() {
  return <div>Hello</div>;
}
 
// ✅ Или используй Record для динамических данных
function MyComponent(props: Record<string, unknown>) {
  return <div>Hello</div>;
}

❌ Антипаттерн: Использование Function вместо конкретной сигнатуры

// ❌ Плохо — Function слишком общий
interface ButtonProps {
  onClick: Function;
}
 
// ✅ Хорошо — конкретная сигнатура
interface ButtonProps {
  onClick: (event: MouseEvent) => void;
}
 
// ✅ Или используй generic для flexibility
interface ButtonProps<T = void> {
  onClick: (event: MouseEvent) => T;
}

❌ Антипаттерн: Неправильный импорт типов

// ❌ Плохо — импортируем типы как values
import { User } from "./types";
 
// ✅ Хорошо — используй type import (помогает bundler'у)
import type { User } from "./types";
 
// ✅ Или inline type import
import { type User, createUser } from "./user";

❌ Антипаттерн: Смешивание type и interface без причины

// ❌ Плохо — непоследовательность
interface User {
  id: number;
}
 
type Post = {
  id: number;
};
 
// ✅ Хорошо — выбери соглашение и придерживайся его
// Вариант 1: Interface для объектов
interface User {
  id: number;
}
 
interface Post {
  id: number;
}
 
// Вариант 2: Type для всего (более гибко)
type User = {
  id: number;
};
 
type Post = {
  id: number;
};
 
// Type для union/intersection
type ID = string | number;

Правило: Interface для публичных API и когда планируется расширение. Type для остального (union, tuple, aliases, utility types).

❌ Антипаттерн: Игнорирование ошибок компилятора

// ❌ Плохо — подавляем ошибки
// @ts-ignore
const result = someFunction();
 
// ❌ Плохо — отключаем проверку для всего файла
// @ts-nocheck
 
// ✅ Хорошо — исправляем типы
const result = someFunction() as ExpectedType;
 
// ✅ Или создаём правильные типы
interface SomeFunctionResult {
  data: string;
}
 
const result: SomeFunctionResult = someFunction();
 
// ✅ Если действительно нужно подавить — добавь комментарий ПОЧЕМУ
// @ts-expect-error - Legacy API, will be fixed in v2
const result = someFunction();

Чек-лист правильного использования TypeScript

DO:

  • Включай strict mode в tsconfig.json
  • Используй unknown вместо any для неизвестных типов
  • Пиши type guards для проверки типов в runtime
  • Используй утилитные типы (Pick, Omit, Partial) вместо дублирования
  • Полагайся на type inference где это очевидно
  • Используй discriminated unions для вариантов
  • Валидируй данные из внешних источников (API, localStorage)

DON'T:

  • Не злоупотребляй any — это отключает весь TypeScript
  • Не используй as без крайней необходимости
  • Не игнорируй ошибки через @ts-ignore
  • Не дублируй типы — используй наследование и utility types
  • Не пиши избыточные аннотации для очевидных типов
  • Не используй Function — пиши конкретные сигнатуры
  • Не смешивай null/undefined проверки — включи strictNullChecks

Практические кейсы из реального мира

Типизация Fetch API и асинхронных операций

// Типобезопасный fetch wrapper
interface ApiError {
  message: string;
  code: string;
  details?: Record<string, unknown>;
}
 
type ApiResponse<T> =
  | { success: true; data: T }
  | { success: false; error: ApiError };
 
async function fetchApi<T>(
  url: string,
  options?: RequestInit
): Promise<ApiResponse<T>> {
  try {
    const response = await fetch(url, options);
 
    if (!response.ok) {
      const error: ApiError = await response.json();
      return { success: false, error };
    }
 
    const data: T = await response.json();
    return { success: true, data };
  } catch (error) {
    return {
      success: false,
      error: {
        message: error instanceof Error ? error.message : "Unknown error",
        code: "NETWORK_ERROR",
      },
    };
  }
}
 
// Использование
interface User {
  id: number;
  name: string;
  email: string;
}
 
const result = await fetchApi<User>("/api/users/1");
 
if (result.success) {
  console.log(result.data.name); // TypeScript знает структуру
} else {
  console.error(result.error.message);
}
 
// Generic функция для всех API эндпоинтов
type APIEndpoints = {
  "/users": User[];
  "/posts": Post[];
  "/comments": Comment[];
};
 
async function get<K extends keyof APIEndpoints>(
  endpoint: K
): Promise<ApiResponse<APIEndpoints[K]>> {
  return fetchApi<APIEndpoints[K]>(endpoint);
}
 
const users = await get("/users"); // TypeScript знает, что вернётся User[]
const posts = await get("/posts"); // TypeScript знает, что вернётся Post[]

Типизация форм с React Hook Form

import { useForm, SubmitHandler } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
 
// Zod схема для валидации
const loginSchema = z.object({
  email: z.string().email("Некорректный email"),
  password: z.string().min(8, "Минимум 8 символов"),
  rememberMe: z.boolean().optional(),
});
 
// Тип выводится из схемы
type LoginFormData = z.infer<typeof loginSchema>;
 
function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<LoginFormData>({
    resolver: zodResolver(loginSchema),
  });
 
  const onSubmit: SubmitHandler<LoginFormData> = async (data) => {
    // data типизирована как LoginFormData
    const result = await fetchApi<{ token: string }>("/api/login", {
      method: "POST",
      body: JSON.stringify(data),
    });
 
    if (result.success) {
      localStorage.setItem("token", result.data.token);
    }
  };
 
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email")} type="email" />
      {errors.email && <span>{errors.email.message}</span>}
 
      <input {...register("password")} type="password" />
      {errors.password && <span>{errors.password.message}</span>}
 
      <label>
        <input {...register("rememberMe")} type="checkbox" />
        Запомнить меня
      </label>
 
      <button type="submit">Войти</button>
    </form>
  );
}

Типизация State Management (Zustand)

import { create } from "zustand";
 
interface User {
  id: number;
  name: string;
  email: string;
}
 
interface AuthState {
  user: User | null;
  token: string | null;
  isLoading: boolean;
  error: string | null;
 
  // Actions
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  setUser: (user: User) => void;
}
 
const useAuthStore = create<AuthState>((set) => ({
  user: null,
  token: null,
  isLoading: false,
  error: null,
 
  login: async (email, password) => {
    set({ isLoading: true, error: null });
 
    const result = await fetchApi<{ user: User; token: string }>("/api/login", {
      method: "POST",
      body: JSON.stringify({ email, password }),
    });
 
    if (result.success) {
      set({
        user: result.data.user,
        token: result.data.token,
        isLoading: false,
      });
    } else {
      set({
        error: result.error.message,
        isLoading: false,
      });
    }
  },
 
  logout: () => {
    set({ user: null, token: null });
    localStorage.removeItem("token");
  },
 
  setUser: (user) => set({ user }),
}));
 
// Использование в компоненте
function Profile() {
  const { user, logout } = useAuthStore();
 
  if (!user) return <div>Войдите в систему</div>;
 
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <button onClick={logout}>Выйти</button>
    </div>
  );
}

Типизация Server Actions (Next.js)

"use server";
 
import { z } from "zod";
 
// Схема валидации
const createPostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(1),
  tags: z.array(z.string()).max(10),
  published: z.boolean(),
});
 
type CreatePostInput = z.infer<typeof createPostSchema>;
 
// Server Action с типизацией
export async function createPost(formData: FormData) {
  // Парсим FormData
  const data = {
    title: formData.get("title"),
    content: formData.get("content"),
    tags: formData.getAll("tags"),
    published: formData.get("published") === "true",
  };
 
  // Валидируем
  const validationResult = createPostSchema.safeParse(data);
 
  if (!validationResult.success) {
    return {
      success: false as const,
      errors: validationResult.error.flatten(),
    };
  }
 
  // Типы проверены, можно использовать
  const post = await db.post.create({
    data: validationResult.data,
  });
 
  return {
    success: true as const,
    data: post,
  };
}
 
// Использование в Client Component
"use client";
 
import { useFormState } from "react-dom";
import { createPost } from "./actions";
 
function CreatePostForm() {
  const [state, formAction] = useFormState(createPost, null);
 
  return (
    <form action={formAction}>
      <input name="title" />
      {state?.errors?.fieldErrors?.title && (
        <span>{state.errors.fieldErrors.title}</span>
      )}
 
      <textarea name="content" />
      <input name="tags" />
 
      <label>
        <input name="published" type="checkbox" value="true" />
        Опубликовать
      </label>
 
      <button type="submit">Создать пост</button>
    </form>
  );
}

Практические кейсы показывают, как TypeScript помогает в реальных задачах: валидация данных, type-safe API, работа с формами и state management.

Тестирование TypeScript кода

Типизация тестов с Vitest/Jest

import { describe, it, expect, vi } from "vitest";
import type { Mock } from "vitest";
 
// Типизация теста
describe("calculateTotal", () => {
  it("should calculate total price", () => {
    const items: CartItem[] = [
      { id: 1, name: "Item 1", price: 100, quantity: 2 },
      { id: 2, name: "Item 2", price: 50, quantity: 1 },
    ];
 
    const total = calculateTotal(items);
 
    expect(total).toBe(250);
  });
 
  it("should return 0 for empty cart", () => {
    const items: CartItem[] = [];
    expect(calculateTotal(items)).toBe(0);
  });
});
 
// Типизация моков
interface UserService {
  getUser: (id: number) => Promise<User>;
  updateUser: (id: number, data: Partial<User>) => Promise<User>;
}
 
describe("UserService", () => {
  it("should fetch user", async () => {
    const mockGetUser = vi.fn<[number], Promise<User>>();
    mockGetUser.mockResolvedValue({
      id: 1,
      name: "Иван",
      email: "ivan@example.com",
    });
 
    const user = await mockGetUser(1);
    expect(user.name).toBe("Иван");
  });
});

Type Testing с tsd или expect-type

Иногда нужно протестировать сами типы — убедиться, что они правильно выводятся.

import { expectType, expectError } from "tsd";
 
// Тест типов
function getUser(id: number) {
  return { id, name: "Иван", email: "ivan@example.com" };
}
 
// Проверяем, что тип выводится правильно
expectType<{ id: number; name: string; email: string }>(getUser(1));
 
// Проверяем, что неправильный тип вызывает ошибку
expectError(getUser("1")); // должна быть ошибка
 
// Тестирование utility types
type UserDTO = Omit<User, "password">;
 
expectType<UserDTO>({ id: 1, name: "Иван", email: "ivan@example.com" });
expectError<UserDTO>({ id: 1, name: "Иван", password: "secret" });

Mocking с типами

import { vi } from "vitest";
 
// Типизированный mock fetch
const mockFetch = vi.fn<[string, RequestInit?], Promise<Response>>();
 
mockFetch.mockResolvedValue({
  ok: true,
  json: async () => ({ id: 1, name: "Иван" }),
} as Response);
 
// Типизированный mock модуля
vi.mock("./api", () => ({
  fetchUser: vi.fn<[number], Promise<User>>(),
  createUser: vi.fn<[CreateUserInput], Promise<User>>(),
}));
 
// Использование
import { fetchUser } from "./api";
 
(fetchUser as Mock).mockResolvedValue({
  id: 1,
  name: "Иван",
  email: "ivan@example.com",
});

Тестирование React компонентов

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Button } from "./Button";
 
describe("Button", () => {
  it("should render with label", () => {
    render(<Button onClick={() => {}}>Click me</Button>);
    expect(screen.getByRole("button")).toHaveTextContent("Click me");
  });
 
  it("should call onClick when clicked", async () => {
    const user = userEvent.setup();
    const handleClick = vi.fn();
 
    render(<Button onClick={handleClick}>Click me</Button>);
 
    await user.click(screen.getByRole("button"));
 
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
 
  it("should be disabled when disabled prop is true", () => {
    render(
      <Button onClick={() => {}} disabled>
        Click me
      </Button>
    );
 
    expect(screen.getByRole("button")).toBeDisabled();
  });
});

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

Часто задаваемые вопросы (FAQ)

Когда использовать interface vs type?

Используй interface когда:

  • Описываешь структуру объекта
  • Планируешь расширение (extends, implements)
  • Нужна возможность declaration merging
  • Пишешь публичный API библиотеки

Используй type когда:

  • Нужны union, intersection, tuple
  • Работаешь с примитивами или литералами
  • Создаёшь utility types
  • Нужны mapped или conditional types
// Interface для объектов
interface User {
  id: number;
  name: string;
}
 
// Type для остального
type ID = string | number;
type Status = "active" | "inactive";
type Point = [number, number];

Нужно ли типизировать всё в коде?

Нет, не всё нужно типизировать явно. TypeScript умеет выводить типы (type inference).

// ❌ Избыточно
const name: string = "Иван";
const age: number = 25;
 
// ✅ Достаточно
const name = "Иван"; // TypeScript знает, что это string
const age = 25; // number
 
// ✅ Типизируй только там, где inference не работает
const users = []; // any[] - здесь нужен тип!
const users: User[] = []; // правильно

Обязательно типизируй:

  • Функции (параметры и возвращаемое значение)
  • Props React компонентов
  • API ответы
  • Public API библиотек

Как работать с динамическими данными из API?

Используй runtime валидацию:

import { z } from "zod";
 
// Схема валидации
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});
 
// Типprouted из схемы
type User = z.infer<typeof UserSchema>;
 
// Валидация данных
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
 
  // Проверка в runtime
  return UserSchema.parse(data);
}

Что делать, если библиотека не имеет типов?

Варианты решения:

  1. Установить типы из @types:
npm install -D @types/lodash
  1. Создать declaration файл:
// types/my-library.d.ts
declare module "my-library" {
  export interface Config {
    apiKey: string;
  }
 
  export function init(config: Config): void;
}
  1. Использовать any временно:
declare module "legacy-library";

Влияет ли TypeScript на производительность?

Нет, TypeScript не влияет на runtime производительность.

  • Типы существуют только на этапе разработки
  • После компиляции получается чистый JavaScript
  • Никаких дополнительных проверок в браузере

Что может влиять:

  • Время компиляции (можно оптимизировать через incremental, skipLibCheck)
  • Размер IDE (TypeScript Language Server требует памяти)

Можно ли использовать TypeScript с JavaScript в одном проекте?

Да, TypeScript поддерживает постепенную миграцию:

{
  "compilerOptions": {
    "allowJs": true, // Разрешить .js файлы
    "checkJs": false // Не проверять JS файлы (опционально)
  }
}

Можешь мигрировать файл за файлом:

  • .js.ts
  • .jsx.tsx

Как типизировать this в функциях?

interface User {
  name: string;
  greet(this: User): void;
}
 
const user: User = {
  name: "Иван",
  greet() {
    console.log(`Hello, ${this.name}`);
  },
};
 
user.greet(); // OK
const greet = user.greet;
greet(); // Error: The 'this' context is not of type 'User'

Можно ли расширить встроенные типы?

Да, через declaration merging:

// Расширение глобальных объектов
interface Window {
  myCustomProperty: string;
}
 
window.myCustomProperty = "value"; // OK
 
// Расширение Array
interface Array<T> {
  first(): T | undefined;
}
 
Array.prototype.first = function () {
  return this[0];
};
 
[1, 2, 3].first(); // 1

Что такое never и когда его использовать?

never — это тип, который никогда не наступает.

// Функция, которая никогда не возвращает значение
function throwError(message: string): never {
  throw new Error(message);
}
 
// Exhaustive checking в switch
type Shape = "circle" | "square";
 
function getArea(shape: Shape) {
  switch (shape) {
    case "circle":
      return Math.PI;
    case "square":
      return 1;
    default:
      const _exhaustive: never = shape; // Проверка, что все варианты обработаны
      throw new Error(`Unknown shape: ${_exhaustive}`);
  }
}

Как типизировать динамические ключи объекта?

// Index signature
interface Dictionary {
  [key: string]: string;
}
 
const translations: Dictionary = {
  hello: "Привет",
  goodbye: "Пока",
};
 
// Record utility type
type Translation = Record<string, string>;
 
// Mapped type для конкретных ключей
type Colors = "red" | "green" | "blue";
type ColorMap = Record<Colors, string>;
 
const colors: ColorMap = {
  red: "#FF0000",
  green: "#00FF00",
  blue: "#0000FF",
};

Большинство проблем с TypeScript решаются через правильное использование утилитных типов, generics и валидацию данных. Если застрял — посмотри официальную документацию или примеры в DefinitelyTyped.

Заключение

TypeScript — это не просто "JavaScript с типами", это инструмент, который:

  • Ловит ошибки на этапе разработки — до того, как код попадёт в продакшн
  • Улучшает developer experience — автодополнение, рефакторинг, inline документация
  • Упрощает командную работу — типы как контракт между разработчиками
  • Делает код самодокументируемым — не нужны JSDoc комментарии

С чего начать

  1. Установи TypeScript в новый или существующий проект
  2. Начни с простого — типизируй функции и props компонентов
  3. Изучи базовые типы — string, number, array, object
  4. Используй интерфейсы для структур данных
  5. Освой утилитные типы — Pick, Omit, Partial
  6. Практикуйся с generics когда появится необходимость

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

Полезные ресурсы

Удачи в изучении TypeScript! 🚀