Система типов TypeScript исключительно мощная и позволяет применять паттерны, которые выявляют ошибки на этапе компиляции, а не во время выполнения. Переход от базовых типов к продвинутым паттернам существенно повышает качество кода и удобство разработки. В этом руководстве рассматриваются паттерны TypeScript, которыми должен владеть каждый senior-разработчик.
Зачем нужен продвинутый TypeScript
Освоение продвинутого TypeScript позволяет:
- Безопасность на этапе компиляции: выявлять ошибки до попадания в production
- Лучший IntelliSense: IDE понимает структуру вашего кода
- Самодокументируемый код: типы описывают намерение
- Уверенность при рефакторинге: проверка типов выявляет ломающие изменения
- Проектирование API: выражать ограничения в системе типов
Utility Types
Встроенные Utility Types
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// Partial: все свойства опциональны
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string; ... }
// Required: все свойства обязательны
type CompleteUser = Required<Partial<User>>;
// Pick: выбрать конкретные свойства
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: number; name: string; }
// Omit: исключить конкретные свойства
type UserWithoutPassword = Omit<User, 'password'>;
// { id: number; name: string; email: string; createdAt: Date; }
// Readonly: все свойства только для чтения
type ImmutableUser = Readonly<User>;
// Нельзя переназначать свойства
// Record: создать тип объекта с ключами и типом значения
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;
// { [key: string]: 'admin' | 'user' | 'guest' }
// Extract: извлечь типы из union
type StringOrNumber = string | number | boolean;
type OnlyStrings = Extract<StringOrNumber, string>;
// string
// Exclude: удалить типы из union
type NotString = Exclude<StringOrNumber, string>;
// number | boolean
// NonNullable: удалить null и undefined
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// string
// ReturnType: получить тип возвращаемого значения функции
function createUser() {
return { id: 1, name: 'John' };
}
type NewUser = ReturnType<typeof createUser>;
// { id: number; name: string; }
// Parameters: получить типы параметров функции
function greet(name: string, age: number) {}
type GreetParams = Parameters<typeof greet>;
// [string, number]
Generics
Базовые Generics
// Generic-функция
function identity<T>(value: T): T {
return value;
}
const num = identity(42); // number
const str = identity('hello'); // string
// Generic-интерфейс
interface Container<T> {
value: T;
getValue(): T;
}
// Generic-класс
class Box<T> {
constructor(private content: T) {}
getContent(): T {
return this.content;
}
}
const numberBox = new Box(123);
const stringBox = new Box('hello');
Ограниченные Generics
// Ограничение с extends
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): void {
console.log(item.length);
}
logLength('hello'); // OK
logLength([1, 2, 3]); // OK
logLength({ length: 5 }); // OK
// logLength(123); // Error: у number нет length
// Несколько ограничений
interface HasId {
id: number;
}
interface HasName {
name: string;
}
function merge<T extends HasId, U extends HasName>(a: T, b: U): T & U {
return { ...a, ...b };
}
// Ограничение через keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'John', age: 30 };
const name = getProperty(user, 'name'); // string
const age = getProperty(user, 'age'); // number
// getProperty(user, 'invalid'); // Error
Generic-типы по умолчанию
interface ApiResponse<T = unknown> {
data: T;
status: number;
message: string;
}
// Используется значение по умолчанию
const response: ApiResponse = { data: {}, status: 200, message: 'OK' };
// Явно заданный тип
const userResponse: ApiResponse<User> = {
data: { id: 1, name: 'John', email: '[email protected]', password: '', createdAt: new Date() },
status: 200,
message: 'OK'
};
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
// Практический пример: распаковать Promise
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type PromiseString = Promise<string>;
type Unwrapped = Unwrap<PromiseString>; // string
type NotPromise = Unwrap<number>; // number
// Тип элемента массива
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type StringArrayElement = ArrayElement<string[]>; // string
Дистрибутивные Conditional Types
type ToArray<T> = T extends any ? T[] : never;
// Распределяется по union
type StringOrNumberArray = ToArray<string | number>;
// string[] | number[]
// Предотвратить распределение с помощью []
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Combined = ToArrayNonDist<string | number>;
// (string | number)[]
Mapped Types
Базовые Mapped Types
// Сделать все свойства опциональными
type Optional<T> = {
[K in keyof T]?: T[K];
};
// Сделать все свойства обязательными
type Required<T> = {
[K in keyof T]-?: T[K];
};
// Сделать все свойства только для чтения
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
// Убрать readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
Продвинутые Mapped Types
// Преобразовать типы свойств
type Stringify<T> = {
[K in keyof T]: string;
};
// Отфильтровать свойства по типу значения
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Mixed {
name: string;
age: number;
email: string;
}
type StringProps = OnlyStrings<Mixed>;
// { name: string; email: string; }
// Переименовать свойства
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }
Template Literal Types
Базовые Template Literals
type Greeting = `Hello, ${string}!`;
const valid: Greeting = 'Hello, World!'; // OK
// const invalid: Greeting = 'Hi, World!'; // Error
// Комбинирование union
type Color = 'red' | 'green' | 'blue';
type Size = 'small' | 'medium' | 'large';
type ColoredSize = `${Color}-${Size}`;
// 'red-small' | 'red-medium' | 'red-large' | 'green-small' | ...
// HTTP-методы
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = '/users' | '/posts';
type Route = `${Method} ${Endpoint}`;
Типы обработчиков событий
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
type ChangeEvent = EventName<'change'>; // 'onChange'
// Создать тип обработчика событий
type EventHandlers<T> = {
[K in keyof T as EventName<string & K>]: (value: T[K]) => void;
};
interface FormFields {
name: string;
email: string;
age: number;
}
type FormHandlers = EventHandlers<FormFields>;
// { onName: (value: string) => void; onEmail: (value: string) => void; onAge: (value: number) => void; }
Discriminated Unions
Сопоставление с образцом
interface Loading {
status: 'loading';
}
interface Success<T> {
status: 'success';
data: T;
}
interface Error {
status: 'error';
error: string;
}
type AsyncState<T> = Loading | Success<T> | Error;
function handleState<T>(state: AsyncState<T>): string {
switch (state.status) {
case 'loading':
return 'Loading...';
case 'success':
return `Data: ${JSON.stringify(state.data)}`;
case 'error':
return `Error: ${state.error}`;
}
}
// Проверка исчерпываемости
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`);
}
function handleStateExhaustive<T>(state: AsyncState<T>): string {
switch (state.status) {
case 'loading':
return 'Loading...';
case 'success':
return `Data: ${JSON.stringify(state.data)}`;
case 'error':
return `Error: ${state.error}`;
default:
return assertNever(state); // Ошибка компиляции, если отсутствует case
}
}
Type Guards
Пользовательские Type Guards
interface Cat {
meow(): void;
}
interface Dog {
bark(): void;
}
// Предикат типа
function isCat(animal: Cat | Dog): animal is Cat {
return 'meow' in animal;
}
function makeSound(animal: Cat | Dog) {
if (isCat(animal)) {
animal.meow(); // TypeScript знает, что это Cat
} else {
animal.bark(); // TypeScript знает, что это Dog
}
}
// Функция-утверждение (выбрасывает исключение, если false)
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Value is not a string');
}
}
function processValue(value: unknown) {
assertIsString(value);
// TypeScript знает, что здесь value — string
console.log(value.toUpperCase());
}
Declaration Merging
Расширение существующих типов
// Расширить Window
declare global {
interface Window {
analytics: {
track: (event: string, data?: object) => void;
};
}
}
// Теперь валидно
window.analytics.track('pageview');
// Расширить Express Request
declare namespace Express {
interface Request {
user?: {
id: string;
role: string;
};
}
}
// Расширить module
declare module 'express' {
interface Request {
customProperty: string;
}
}
Практические паттерны
Паттерн Builder с типами
class RequestBuilder<T extends object = {}> {
private config: T = {} as T;
withUrl<U extends string>(url: U): RequestBuilder<T & { url: U }> {
return Object.assign(this, { config: { ...this.config, url } });
}
withMethod<M extends 'GET' | 'POST'>(method: M): RequestBuilder<T & { method: M }> {
return Object.assign(this, { config: { ...this.config, method } });
}
build(): T {
return this.config;
}
}
const request = new RequestBuilder()
.withUrl('/api/users')
.withMethod('GET')
.build();
// Тип: { url: '/api/users'; method: 'GET'; }
Ключевые выводы
- Используйте utility types: не изобретайте заново Partial, Pick, Omit
- Ограничивайте generics:
extends делает типы более полезными - Discriminated unions: мощное сопоставление с образцом
- Type guards: безопасно сужайте типы
- Template literals: типобезопасные манипуляции со строками
- Mapped types: системно преобразуйте форму типов
Система типов TypeScript — это язык внутри языка: вложитесь в её глубокое изучение, и качество вашего кода заметно улучшится.