Реальные паттерны с дженериками
Реальные паттерны
Result<T, E> — обработка ошибок без исключений
Паттерн Result (известный как Either в функциональном программировании) позволяет явно обрабатывать ошибки:
// Определение типа Result
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E }
// Фабричные функции
function ok<T>(value: T): Result<T, never> {
return { ok: true, value }
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error }
}
// Использование
function divide(a: number, b: number): Result<number, string> {
if (b === 0) return err('Деление на ноль')
return ok(a / b)
}
const result = divide(10, 2)
if (result.ok) {
console.log(result.value) // 5 — TypeScript знает, что value: number
} else {
console.error(result.error) // TypeScript знает, что error: string
}
Nullable<T> и Optional<T>
// Nullable — может быть null
type Nullable<T> = T | null
// Optional — может быть undefined
type Optional<T> = T | undefined
// NonNullable — убирает null и undefined (встроенный)
type NonNullable<T> = T extends null | undefined ? never : T
// Пример использования
interface ApiUser {
id: number
name: string
avatar: Nullable<string> // аватар может отсутствовать
bio: Optional<string> // биография может быть не задана
}
function getAvatarUrl(avatar: Nullable<string>): string {
return avatar ?? '/default-avatar.png'
}
DeepReadonly<T>
// Рекурсивный readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P]
}
interface Config {
server: {
host: string
port: number
ssl: {
enabled: boolean
cert: string
}
}
database: {
url: string
}
}
type FrozenConfig = DeepReadonly<Config>
const config: FrozenConfig = {
server: {
host: 'localhost',
port: 3000,
ssl: { enabled: true, cert: '/path/to/cert' }
},
database: { url: 'postgres://...' }
}
// config.server.host = 'other' // Ошибка!
// config.server.ssl.enabled = false // Ошибка!
В TypeScript 4.9+ встроен
Readonly<T>, но он нерекурсивный. DeepReadonly<T> — полезный паттерн для иммутабельных конфигураций.Repository<T> — паттерн для работы с данными
// Базовая сущность с идентификатором
interface Entity {
id: number | string
}
// Generic репозиторий
interface Repository<T extends Entity> {
findById(id: T['id']): Promise<T | null>
findAll(filter?: Partial<T>): Promise<T[]>
create(data: Omit<T, 'id'>): Promise<T>
update(id: T['id'], data: Partial<Omit<T, 'id'>>): Promise<T | null>
delete(id: T['id']): Promise<boolean>
}
// Конкретная реализация
interface Post extends Entity {
id: number
title: string
content: string
authorId: number
publishedAt: Date | null
}
class PostRepository implements Repository<Post> {
private posts: Post[] = []
private nextId = 1
async findById(id: number): Promise<Post | null> {
return this.posts.find(p => p.id === id) ?? null
}
async findAll(filter?: Partial<Post>): Promise<Post[]> {
if (!filter) return [...this.posts]
return this.posts.filter(post =>
Object.entries(filter).every(([key, value]) =>
post[key as keyof Post] === value
)
)
}
async create(data: Omit<Post, 'id'>): Promise<Post> {
const post: Post = { ...data, id: this.nextId++ }
this.posts.push(post)
return post
}
async update(id: number, data: Partial<Omit<Post, 'id'>>): Promise<Post | null> {
const index = this.posts.findIndex(p => p.id === id)
if (index < 0) return null
this.posts[index] = { ...this.posts[index], ...data }
return this.posts[index]
}
async delete(id: number): Promise<boolean> {
const before = this.posts.length
this.posts = this.posts.filter(p => p.id !== id)
return this.posts.length < before
}
}
Дженерики отлично подходят для реализации паттерна Builder с типобезопасностью:
// Builder, который отслеживает установленные поля
class QueryBuilder<T extends object, Set extends keyof T = never> {
private conditions: Partial<T> = {}
where<K extends keyof T>(
key: K,
value: T[K]
): QueryBuilder<T, Set | K> {
(this.conditions as any)[key] = value
return this as any
}
build(): Pick<T, Set> {
return this.conditions as Pick<T, Set>
}
}
interface UserQuery {
name: string
age: number
city: string
}
const query = new QueryBuilder<UserQuery>()
.where('name', 'Иван')
.where('city', 'Москва')
.build()
// Тип: { name: string, city: string }
// TypeScript знает, что установлены только name и city