Модуль 12.3
Продвинутые паттерны типизации
Мощь и гибкость системы типов TypeScript
Введение
Добро пожаловать в мир продвинутой типизации TypeScript! Если базовые типы — это фундамент, то продвинутые паттерны — это архитектура, которая позволяет создавать мощные, переиспользуемые и безопасные абстракции.
В этом уроке мы изучим инструменты, которые превращают TypeScript из простого линтера в настоящий язык программирования на уровне типов. Вы научитесь создавать типы, которые адаптируются к данным, извлекают информацию из других типов и обеспечивают максимальную безопасность.
1. Conditional Types (Условные типы)
1.1 Основы Conditional Types
Conditional Types позволяют создавать типы на основе условий — это трёхсторонний оператор для типов:
// Синтаксис Conditional Types:
// T extends U ? X : Y
// Простой пример:
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false
type Test3 = IsString<'hello'>; // true (литеральный тип — подтип string)
// Практический пример: определение типа возвращаемого значения
type ApiResponse<T> = T extends 'user'
? { id: number; name: string; email: string }
: T extends 'product'
? { id: number; title: string; price: number }
: T extends 'order'
? { id: number; total: number; status: string }
: never;
type UserResponse = ApiResponse<'user'>;
// Результат: { id: number; name: string; email: string }
type ProductResponse = ApiResponse<'product'>;
// Результат: { id: number; title: string; price: number }
1.2 Infer в Conditional Types
Ключевое слово infer позволяет «извлечь» тип из другого типа внутри условий:
// 1. Извлечение типа возвращаемого значения функции
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = () => string;
type Result = ReturnOf<Fn>; // string
// 2. Извлечение типа параметра функции
type FirstParam<T> = T extends (first: infer P, ...rest: any[]) => any ? P : never;
type Fn2 = (name: string, age: number) => void;
type Param = FirstParam<Fn2>; // string
// 3. Извлечение элемента массива
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Numbers = number[];
type Elem = ElementOf<Numbers>; // number
// 4. Извлечение промиса
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type PromiseStr = Promise<Promise<string>>;
type Unwrapped = Awaited<PromiseStr>; // string
// 5. Извлечение значений объекта
type ValueOf<T> = T extends { [key: string]: infer V } ? V : never;
type Obj = { a: string; b: number; c: boolean };
type Values = ValueOf<Obj>; // string | number | boolean
// 6. Извлечение ключей по значению
type KeysOfType<T, U> = { [K in keyof T]: T[K] extends U ? K : never }[keyof T];
interface User {
name: string;
age: number;
email: string;
isAdmin: boolean;
}
type StringKeys = KeysOfType<User, string>; // "name" | "email"
type NumberKeys = KeysOfType<User, number>; // "age"
1.3 Distributive Conditional Types
// Когда T — union тип, Conditional Type применяется к каждому элементу type ToArray<T> = T extends any ? T[] : never; //istributiveness: применяется к каждому элементу union type StrArr = ToArray<string>; // string[] type NumArr = ToArray<number>; // number[] type UnionArr = ToArray<string | number>; // string[] | number[] // Чтобы отключить distributiveness, оберните T в кортеж: type ToArrayNonDist<T> = [T] extends [any] ? T[] : never; type Result = ToArrayNonDist<string | number>; // (string | number)[] // Практический пример: фильтрация типов type ExcludeType<T, U> = T extends U ? never : T; type All = 'a' | 'b' | 'c' | 'd'; type WithoutB = ExcludeType<All, 'b'>; // 'a' | 'c' | 'd' // TypeScript уже имеет встроенные Exclude и Extract: type Exclude<T, U> = T extends U ? never : T; type Extract<T, U> = T extends U ? T : never;
2. Mapped Types (Типы с маппингом)
2.1 Основы Mapped Types
// Mapped Types позволяют создавать новые типы, маппя существующие
// { [K in keyof T]: NewType }
// 1. Создание ReadOnly версии
type ReadOnly<T> = {
readonly [K in keyof T]: T[K];
};
interface User {
name: string;
age: number;
email: string;
}
type ReadOnlyUser = ReadOnly<User>;
// Результат:
// {
// readonly name: string;
// readonly age: number;
// readonly email: string;
// }
// 2. Создание Optional версии
type Optional<T> = {
[K in keyof T]?: T[K];
};
type OptionalUser = Optional<User>;
// Результат:
// {
// name?: string;
// age?: number;
// email?: string;
// }
// 3. Комбинирование модификаторов
type ReadOnlyOptional<T> = {
readonly [K in keyof T]?: T[K];
};
// 4. Удаление модификаторов
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
type RequiredMutable<T> = {
[K in keyof T]-?: T[K];
};
2.2 Key Remapping
// Key Remapping (as) — переименование ключей
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User {
name: string;
age: number;
email: string;
}
type UserGetters = Getters<User>;
// Результат:
// {
// getName: () => string;
// getAge: () => number;
// getEmail: () => string;
// }
// Фильтрация ключей
type StringKeys<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type UserStringKeys = StringKeys<User>;
// Результат:
// {
// name: string;
// email: string;
// }
// Удаление определённых ключей
type OmitByValue<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K];
};
interface Mixed {
name: string;
age: number;
active: boolean;
email: string;
}
type NonStringKeys = OmitByValue<Mixed, string>;
// Результат:
// {
// age: number;
// active: boolean;
// }
3. Template Literal Types
// 1. Базовый синтаксис
type EventName = 'click' | 'scroll' | 'mousemove';
type EventHandler = `on${Capitalize<EventName>}`;
// Результат: "onClick" | "onScroll" | "onMousemove"
// 2. Комбинация с Mapped Types
type PropEventSource<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]: (value: T[K]) => void;
};
interface User {
name: string;
age: number;
email: string;
}
type UserEvents = PropEventSource<User>;
// Результат:
// {
// onName: (value: string) => void;
// onAge: (value: number) => void;
// onEmail: (value: string) => void;
// }
// 3. Паттерны для путей
type Path = 'users' | 'posts' | 'comments';
type SubPath = 'list' | 'detail' | 'create';
type FullPath = `${Path}/${SubPath}`;
// Результат: "users/list" | "users/detail" | "users/create" | ...
// 4. API Endpoints
type Resource = 'user' | 'product' | 'order';
type Method = 'get' | 'create' | 'update' | 'delete';
type Endpoint = `/api/${Resource}/${Method}`;
// Результат: "/api/user/get" | "/api/user/create" | ...
// 5. CSS классы
type Size = 'sm' | 'md' | 'lg';
type Variant = 'primary' | 'secondary' | 'danger';
type ButtonClass = `btn-${Size}-${Variant}`;
// Результат: "btn-sm-primary" | "btn-sm-secondary" | ...
// 6. Строгая типизация CSS свойств
type CSSProperty = 'margin' | 'padding';
type Direction = 'top' | 'right' | 'bottom' | 'left';
type CSSSpacing = `${CSSProperty}-${Direction}`;
// Результат: "margin-top" | "margin-right" | ...
// 7. Извлечение подстрок
type ExtractAfter<T extends string, D extends string> =
T extends `${D}${infer Rest}` ? Rest : never;
type Result1 = ExtractAfter<'hello-world', 'hello-'>; // "world"
type Result2 = ExtractAfter<'prefix_value', 'prefix_'>; // "value"
4. Utility Typesподробно
4.1 Встроенные Utility Types
interface User {
id: number;
name: string;
email: string;
age: number;
role: 'admin' | 'user' | 'moderator';
}
// 1. Partial<T> — все свойства опциональны
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; age?: number; role?: ... }
// 2. Required<T> — все свойства обязательны
interface Config {
host?: string;
port?: number;
debug?: boolean;
}
type FullConfig = Required<Config>;
// { host: string; port: number; debug: boolean }
// 3. Pick<T, K> — выбор подмножества свойств
type UserBasic = Pick<User, 'id' | 'name'>;
// { id: number; name: string }
// 4. Omit<T, K> — исключение свойств
type UserWithoutEmail = Omit<User, 'email'>;
// { id: number; name: string; age: number; role: ... }
// 5. Record<K, V> — объект с фиксированными ключами и значениями
type UserRoles = Record<'admin' | 'user' | 'moderator', string[]>;
// { admin: string[]; user: string[]; moderator: string[] }
// 6. Readonly<T> — все свойства readonly
type FrozenUser = Readonly<User>;
// { readonly id: number; readonly name: string; ... }
// 7. ReadonlyArray<T> — массив только для чтения
type ReadonlyUsers = ReadonlyArray<User>;
// 8. ReturnType<T> — тип возвращаемого значения функции
function getUser() {
return { id: 1, name: 'Alice', email: 'alice@example.com' };
}
type UserReturn = ReturnType<typeof getUser>;
// { id: number; name: string; email: string }
// 9. Parameters<T> — типы параметров функции
function createUser(name: string, age: number) {
return { name, age };
}
type CreateUserParams = Parameters<typeof createUser>;
// [string, number]
// 10. ConstructorParameters<T> — параметры конструктора класса
class Box {
constructor(public width: number, public height: number) {}
}
type BoxParams = ConstructorParameters<typeof Box>;
// [number, number]
4.2 Создание собственных Utility Types
// 1. DeepPartial — рекурсивный Partial
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
interface Config {
database: {
host: string;
port: number;
credentials: {
user: string;
password: string;
};
};
server: {
port: number;
};
}
type PartialConfig = DeepPartial<Config>;
// Все вложенные свойства также опциональны
// 2. DeepReadonly — рекурсивный Readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
// 3. Prettify — улучшение вывода типов (для IDE)
type Prettify<T> = { [K in keyof T]: T[K] } & {};
// 4. Merge — слияние двух типов (второй перезаписывает первый)
type Merge<A, B> = Omit<A, keyof B> & B;
// 5. StrictOmit — Omit с проверкой существования ключей
type StrictOmit<T, K extends keyof T> = Omit<T, K>
// 6. RequiredKeys — извлечение обязательных ключей
type RequiredKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K
}[keyof T];
// 7. OptionalKeys — извлечение опциональных ключей
type OptionalKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? K : never
}[keyof T];
// 8. NonNullable — удаление null и undefined
type NonNullableDeep<T> = {
[K in keyof T]: NonNullable<T[K]>;
};
// 9. AtLeast — минимум N свойств обязательны
type AtLeast<T, Keys extends keyof T = keyof T> =
Required<Pick<T, Keys>> & Partial<Omit<T, Keys>>;
// Пример: хотя бы одно из name/email обязательно
type UserInput = AtLeast<User, 'name' | 'email'>;
// Должно содержать name ИЛИ email (или оба)
5. Дженерики продвинутого уровня
// 1. Constraints (ограничения)
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Alice', age: 30, email: 'alice@example.com' };
const name = getProperty(user, 'name'); // string (тип безопасно извлечён)
const age = getProperty(user, 'age'); // number
// 2. Default generic parameters
type ApiResponse<T = unknown> = {
data: T;
status: number;
message: string;
};
type UserResponse = ApiResponse<User>; // T = User
type GenericResponse = ApiResponse<>; // T = unknown
// 3. Conditional generic types
type Container<T> = T extends string
? { text: T }
: T extends number
? { value: T }
: T extends boolean
? { flag: T }
: { data: T };
type StringContainer = Container<'hello'>; // { text: 'hello' }
type NumberContainer = Container<42>; // { value: 42 }
// 4. Mapped generic types
type Nullable<T> = { [K in keyof T]: T[K] | null };
// 5. Recursive generic types
type DeepReadonlyArray<T> = T extends (infer U)[]
? ReadonlyArray<DeepReadonlyArray<U>>
: T extends object
? { readonly [K in keyof T]: DeepReadonlyArray<T[K]> }
: T;
// 6. Generic constraints с multiple types
function merge<A extends object, B extends object>(a: A, b: B): A & B {
return { ...a, ...b };
}
// 7. Generic utility для типизации событий
type EventMap = {
click: { x: number; y: number };
keydown: { key: string; code: string };
scroll: { scrollTop: number };
};
type EventHandler<K extends keyof EventMap> = (event: EventMap[K]) => void;
const handleClick: EventHandler<'click'> = (event) => {
console.log(event.x, event.y); // TypeScript знает тип
};
6. Type Guards и сужение типов
// 1. typeof guard
function processValue(value: string | number | boolean) {
if (typeof value === 'string') {
// TypeScript знает, что value — string
return value.toUpperCase();
} else if (typeof value === 'number') {
// TypeScript знает, что value — number
return value.toFixed(2);
} else {
// TypeScript знает, что value — boolean
return value ? 'Yes' : 'No';
}
}
// 2. instanceof guard
class ApiError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
}
}
class ValidationError extends Error {
constructor(public fields: string[]) {
super('Validation failed');
}
}
function handleError(error: Error) {
if (error instanceof ApiError) {
console.log(`API Error ${error.statusCode}: ${error.message}`);
} else if (error instanceof ValidationError) {
console.log(`Validation failed for: ${error.fields.join(', ')}`);
} else {
console.log(`Unknown error: ${error.message}`);
}
}
// 3. Custom Type Guard (function с type predicate)
interface Cat {
type: 'cat';
meow: () => void;
}
interface Dog {
type: 'dog';
bark: () => void;
}
type Pet = Cat | Dog;
function isCat(pet: Pet): pet is Cat {
return pet.type === 'cat';
}
function isDog(pet: Pet): pet is Dog {
return pet.type === 'dog';
}
function handlePet(pet: Pet) {
if (isCat(pet)) {
pet.meow(); // TypeScript знает, что это Cat
} else {
pet.bark(); // TypeScript знает, что это Dog
}
}
// 4. Assertion function
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error(`Expected string, got ${typeof value}`);
}
}
function process(input: unknown) {
assertIsString(input); // После этой строки input — string
console.log(input.toUpperCase()); // Безопасно
}
// 5. Discriminated Union с Type Guard
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number }
| { kind: 'triangle'; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return (shape.base * shape.height) / 2;
}
}
7. Discriminated Unions (Дискриминированные объединения)
// Discriminated Unions — один из самых мощных паттернов TypeScript
// Пример 1: Состояния загрузки
type LoadingState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: User[] }
| { status: 'error'; error: string };
function renderState(state: LoadingState) {
switch (state.status) {
case 'idle':
return 'Ready to load';
case 'loading':
return 'Loading...';
case 'success':
// TypeScript знает, что state.data существует
return `Loaded ${state.data.length} users`;
case 'error':
// TypeScript знает, что state.error существует
return `Error: ${state.error}`;
}
}
// Пример 2: API Response
type ApiResponse<T> =
| { success: true; data: T; timestamp: number }
| { success: false; error: string; code: number };
function handleResponse(response: ApiResponse<User[]>) {
if (response.success) {
// Narrowing: response.data и response.timestamp доступны
console.log(`Got ${response.data.length} users at ${response.timestamp}`);
} else {
// Narrowing: response.error и response.code доступны
console.error(`Error ${response.code}: ${response.error}`);
}
}
// Пример 3: Form Events
type FormEvent =
| { type: 'submit'; formData: Record<string, string> }
| { type: 'change'; field: string; value: string }
| { type: 'focus'; field: string }
| { type: 'blur'; field: string };
function handleFormEvent(event: FormEvent) {
switch (event.type) {
case 'submit':
console.log('Submitting:', event.formData);
break;
case 'change':
console.log(`Field ${event.field} changed to ${event.value}`);
break;
case 'focus':
console.log(`Field ${event.field} focused`);
break;
case 'blur':
console.log(`Field ${event.field} blurred`);
break;
}
}
8. Branded Types и Nominal Typing
// Проблема: TypeScript использует structural typing
// Два типа с одинаковой структурой считаются совместимыми
type UserId = string;
type OrderId = string;
function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }
const userId: UserId = 'user-123';
const orderId: OrderId = 'order-456';
// Это работает, хотя мы не хотим путать ID:
getUser(orderId); // ОК для TypeScript, но это ошибка в логике!
getOrder(userId); // Аналогично
// Решение: Branded Types
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
// Фабричные функции для создания branded типов
function createUserId(id: string): UserId {
// Здесь можно добавить валидацию
if (!id.startsWith('user-')) {
throw new Error('Invalid user ID format');
}
return id as UserId;
}
function createOrderId(id: string): OrderId {
if (!id.startsWith('order-')) {
throw new Error('Invalid order ID format');
}
return id as OrderId;
}
// Теперь это НЕ скомпилируется:
// getUser(orderId); // Error: OrderId не assignable к UserId
// getOrder(userId); // Error: UserId не assignable к OrderId
// Только правильные вызовы:
const userId = createUserId('user-123');
const orderId = createOrderId('order-456');
getUser(userId); // OK
getOrder(orderId); // OK
// Пример: Email branded type
type Email = Brand<string, 'Email'>;
function createEmail(email: string): Email {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new Error('Invalid email format');
}
return email as Email;
}
// Пример: PositiveNumber branded type
type PositiveNumber = Brand<number, 'PositiveNumber'>;
function createPositiveNumber(n: number): PositiveNumber {
if (n <= 0) {
throw new Error('Number must be positive');
}
return n as PositiveNumber;
}
9. Паттерны проектирования с типами
// 1. Builder Pattern с типами
class QueryBuilder<T extends Record<string, any>> {
private filters: Partial<T> = {};
private sortOrder: keyof T | null = null;
private limitValue: number = 100;
where<K extends keyof T>(key: K, value: T[K]): this {
this.filters[key] = value;
return this;
}
orderBy(key: keyof T): this {
this.sortOrder = key;
return this;
}
limit(n: number): this {
this.limitValue = n;
return this;
}
build(): { filters: Partial<T>; sort: keyof T | null; limit: number } {
return {
filters: this.filters,
sort: this.sortOrder,
limit: this.limitValue,
};
}
}
// Использование
interface UserFilter {
name: string;
age: number;
role: 'admin' | 'user';
}
const query = new QueryBuilder<UserFilter>()
.where('role', 'admin')
.where('age', 30)
.orderBy('name')
.limit(10)
.build();
// 2. Factory Pattern с типами
type AnimalType = 'dog' | 'cat' | 'bird';
interface Animal {
type: AnimalType;
name: string;
sound: string;
}
class AnimalFactory {
static create(type: AnimalType, name: string): Animal {
const animals: Record<AnimalType, Omit<Animal, 'name'>> = {
dog: { type: 'dog', sound: 'Woof' },
cat: { type: 'cat', sound: 'Meow' },
bird: { type: 'bird', sound: 'Tweet' },
};
return { ...animals[type], name };
}
}
const dog = AnimalFactory.create('dog', 'Rex');
// { type: 'dog', name: 'Rex', sound: 'Woof' }
// 3. Strategy Pattern с типами
interface SortStrategy<T> {
compare(a: T, b: T): number;
}
class Sorter<T> {
constructor(private strategy: SortStrategy<T>) {}
sort(items: T[]): T[] {
return [...items].sort((a, b) => this.strategy.compare(a, b));
}
setStrategy(strategy: SortStrategy<T>): void {
this.strategy = strategy;
}
}
const numericSort: SortStrategy<number> = {
compare: (a, b) => a - b,
};
const stringSort: SortStrategy<string> = {
compare: (a, b) => a.localeCompare(b),
};
const sorter = new Sorter<number>(numericSort);
sorter.sort([3, 1, 4, 1, 5]);
10. Типизация middleware и цепочек вызовов
// 1. Middleware типизация
interface Request {
body: any;
params: Record<string, string>;
query: Record<string, string>;
user?: { id: string; role: string };
}
interface Response {
status: (code: number) => Response;
json: (data: any) => void;
}
type Middleware = (req: Request, res: Response, next: () => void) => void;
// 2. Типизированная цепочка middleware
class Pipeline<TInput, TOutput> {
private middlewares: Array<(input: TInput) => TInput | TOutput> = [];
use<TNext>(fn: (input: TInput) => TNext): Pipeline<TInput, TNext> {
this.middlewares.push(fn as any);
return this as any;
}
execute(input: TInput): TOutput {
let result: any = input;
for (const middleware of this.middlewares) {
result = middleware(result);
}
return result;
}
}
// Использование
interface RawData {
data: string;
}
interface ParsedData {
data: Record<string, any>;
}
interface ValidatedData extends ParsedData {
isValid: boolean;
}
const pipeline = new Pipeline<RawData, ValidatedData>()
.use((input: RawData): ParsedData => ({
data: JSON.parse(input.data),
}))
.use((input: ParsedData): ValidatedData => ({
...input,
isValid: Object.keys(input.data).length > 0,
}));
const result = pipeline.execute({ data: '{"key": "value"}' });
// result.data is Record<string, any>, result.isValid is boolean
// 3. Builder для HTTP запросов
class RequestBuilder<TBody = never, TResponse = unknown> {
private url: string = '';
private method: string = 'GET';
private body?: TBody;
private headers: Record<string, string> = {};
setUrl(url: string): this {
this.url = url;
return this;
}
setMethod(method: string): this {
this.method = method;
return this;
}
setBody(body: TBody): this {
this.body = body;
return this;
}
setHeader(key: string, value: string): this {
this.headers[key] = value;
return this;
}
async send(): Promise<TResponse> {
const response = await fetch(this.url, {
method: this.method,
headers: this.headers,
body: this.body ? JSON.stringify(this.body) : undefined,
});
return response.json() as Promise<TResponse>;
}
}
// Использование
interface CreateUserRequest {
name: string;
email: string;
}
interface CreateUserResponse {
id: string;
name: string;
}
const user = await new RequestBuilder<CreateUserRequest, CreateUserResponse>()
.setUrl('https://api.example.com/users')
.setMethod('POST')
.setHeader('Content-Type', 'application/json')
.setBody({ name: 'Alice', email: 'alice@example.com' })
.send();
11. Рекурсивные типы
// 1. Рекурсивный DeepPartial
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// 2. Рекурсивный DeepReadonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
// 3. Рекурсивный Flattened (выравнивание вложенных объектов)
type Flattened<T, Prefix extends string = ''> = {
[K in keyof T as Prefix extends '' ? K : `${Prefix}.${K & string}`]:
T[K] extends object ? Flattened<T[K], Prefix extends '' ? K & string : `${Prefix}.${K & string}`> : T[K];
};
interface Nested {
user: {
name: string;
address: {
city: string;
zip: string;
};
};
settings: {
theme: string;
};
}
type Flat = Flattened<Nested>;
// {
// "user": { name: string; address: { city: string; zip: string } };
// "user.name": string;
// "user.address": { city: string; zip: string };
// "user.address.city": string;
// "user.address.zip": string;
// "settings": { theme: string };
// "settings.theme": string;
// }
// 4. Рекурсивное получение всех путей объекта
type Paths<T> = T extends object
? { [K in keyof T]: K extends string
? T[K] extends object
? K | `${K}.${Paths<T[K]>}`
: K
: never
}[keyof T]
: never;
type UserPaths = Paths<Nested>;
// "user" | "user.name" | "user.address" | "user.address.city" | ...
12. Type-level программирование
// Type-level арифметика (ограниченно)
// TypeScript не поддерживает настоящую арифметику типов,
// но можно создать хаки:
// 1. Tuple length как число
type Length<T extends any[]> = T['length'];
type Tuple1 = [1, 2, 3];
type Len = Length<Tuple1>; // 3
// 2. Сравнение длин
type IsEqual<A, B> = A extends B ? (B extends A ? true : false) : false;
type Test1 = IsEqual<string, string>; // true
type Test2 = IsEqual<string, number>; // false
// 3. Concatenation строк (через Template Literal)
type Concat<A extends string, B extends string> = `${A}${B}`;
type Hello = Concat<'Hel', 'lo'>; // "Hello"
// 4. Length строки
type StringLength<S extends string, Acc extends any[] = []> =
S extends `${infer _}${infer Rest}`
? StringLength<Rest, [...Acc, unknown]>
: Acc['length'];
type Len5 = StringLength<'hello'>; // 5
// 5. Реверс строки
type ReverseString<S extends string> =
S extends `${infer First}${infer Rest}`
? `${ReverseString<Rest>}${First}`
: S;
type Reversed = ReverseString<'hello'>; // "olleh"
// 6. Fibonacci на типах (ограниченно)
type Fibonacci<N extends number, A extends any[] = [unknown], B extends any[] = [unknown], Acc extends any[] = []>
= Acc['length'] extends N
? A['length']
: Fibonacci<N, B, [...A, ...B], [...Acc, unknown]>;
type Fib5 = Fibonacci<5>; // 5 (1, 1, 2, 3, 5)
13. Производительность типов
Советы по производительности:
- Избегайте глубокой рекурсии типов
- Используйте
as constвместо широких типов - Не создавайте слишком сложные union типы
- Кэшируйте вычисленные типы
- Используйте
satisfies(TypeScript 4.9+) для проверки без расширения типа
// satisfies (TypeScript 4.9+) — проверка типа без расширения
const colors = {
red: 'rgb(255, 0, 0)',
green: 'rgb(0, 255, 0)',
blue: 'rgb(0, 0, 255)',
} satisfies Record<string, string>;
// TypeScript проверяет, что colors соответствует Record<string, string>
// но тип keys сохраняется как 'red' | 'green' | 'blue'
colors.red; // ОК
colors.yellow; // Error!
// Без satisfies тип был бы Record<string, string>
// и colors.red возвращал бы string, а не 'rgb(255, 0, 0)'
Итоги урока
В этом уроке мы изучили продвинутые паттерны типизации TypeScript:
- Conditional Types — условия в типах с infer для извлечения
- Mapped Types — маппинг свойств с модификаторами
- Template Literal Types — строковые типы на основе шаблонов
- Utility Types — Partial, Required, Pick, Omit и создание собственных
- Продвинутые дженерики — ограничения, default параметры, conditional
- Type Guards — typeof, instanceof, custom type predicates
- Discriminated Unions — безопасное сужение типов
- Branded Types — nominal typing для безопасности
- Паттерны — Builder, Factory, Strategy с типами
- Рекурсивные типы — глубокая обработка объектов
- Type-level — программирование на уровне типов
Эти техники превращают TypeScript из простого линтера в мощный инструмент для создания безопасных и переиспользуемых абстракций. В следующем уроке мы рассмотрим частые ошибки и как их избегать.
Продвинутые паттерны типизации
10 вопросов