$ sudo teach IT

Модуль 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 вопросов

Discriminated Unions и Branded Types

Premium