Без generics приходится выбирать: писать код под конкретный тип или терять типобезопасность через any:
// Проблема: функция работает только со string
function firstString(arr: string[]): string {
return arr[0]
}
// Антипаттерн: теряем типобезопасность
function firstAny(arr: any[]): any {
return arr[0] // возвращает any — TS не знает тип
}
// Решение: generic — T подставляется при вызове
function first<T>(arr: T[]): T {
return arr[0]
}
const s = first(['a', 'b', 'c']) // T = string, возвращает string
const n = first([1, 2, 3]) // T = number, возвращает numberGenerics = пишем код один раз, работает с любым типом **без потери типобезопасности**.
// Generic функция
function identity<T>(arg: T): T {
return arg
}
// Явное указание типа
const result1 = identity<string>('hello') // T = string
// Вывод типа (type inference) — TypeScript сам определяет T
const result2 = identity(42) // T = number (автоматически)// Generic интерфейс
interface Repository<T> {
findById(id: number): T | undefined
findAll(): T[]
save(item: T): void
}
// Generic класс
class Stack<T> {
private items: T[] = []
push(item: T): void {
this.items.push(item)
}
pop(): T | undefined {
return this.items.pop()
}
peek(): T | undefined {
return this.items[this.items.length - 1]
}
get size(): number {
return this.items.length
}
}
const numStack = new Stack<number>()
numStack.push(1)
numStack.push(2)
console.log(numStack.pop()) // 2 — TypeScript знает что это numberИногда нужно гарантировать что T имеет определённые свойства:
// T должен иметь поле length
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b
}
longest('hello', 'hi') // OK — string имеет length
longest([1, 2, 3], [4, 5]) // OK — array имеет length
// longest(1, 2) // Ошибка TS: number не имеет length
// keyof — ограничение ключами объекта
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: 'Алексей', age: 30 }
getProperty(user, 'name') // string
getProperty(user, 'age') // number
// getProperty(user, 'email') // Ошибка TS: 'email' не существуетfunction zip<T, U>(arr1: T[], arr2: U[]): [T, U][] {
return arr1.map((item, i) => [item, arr2[i]])
}
zip([1, 2, 3], ['a', 'b', 'c'])
// [[1, 'a'], [2, 'b'], [3, 'c']]
// Pair с двумя разными типами
class Pair<A, B> {
constructor(public first: A, public second: B) {}
swap(): Pair<B, A> {
return new Pair(this.second, this.first)
}
}// T = string если не указан явно
interface ApiResponse<T = string> {
data: T
status: number
}
const r1: ApiResponse = { data: 'hello', status: 200 } // T = string
const r2: ApiResponse<number[]> = { data: [1, 2], status: 200 } // T = number[]// Async wrapper — возвращает [data, error]
async function tryCatch<T>(
promise: Promise<T>
): Promise<[T | null, Error | null]> {
try {
const data = await promise
return [data, null]
} catch (err) {
return [null, err as Error]
}
}
const [data, error] = await tryCatch(fetch('/api/users'))
if (error) {
console.error(error.message)
} else {
console.log(data) // TypeScript знает что это Response
}Generic-стиль структуры данных: Stack, Queue, Pair с runtime type tracking
// В TypeScript это было бы Stack<T>, Queue<T>, Pair<A,B>
// В JS эмулируем через runtime проверку типов
class Stack {
#items = []
#typeName = null // запоминаем тип первого элемента
push(item) {
const itemType = Array.isArray(item) ? 'array' : typeof item
if (this.#typeName === null) {
this.#typeName = itemType // определяем тип по первому элементу
} else if (itemType !== this.#typeName) {
throw new TypeError(
`Stack<${this.#typeName}>: нельзя добавить ${itemType}`
)
}
this.#items.push(item)
return this
}
pop() {
if (this.#items.length === 0) return undefined
return this.#items.pop()
}
peek() {
return this.#items[this.#items.length - 1]
}
get size() { return this.#items.length }
get isEmpty() { return this.#items.length === 0 }
get type() { return this.#typeName ?? 'unknown' }
toString() {
return `Stack<${this.type}>([${this.#items.join(', ')}])`
}
}
class Queue {
#items = []
#typeName = null
enqueue(item) {
const itemType = Array.isArray(item) ? 'array' : typeof item
if (this.#typeName === null) {
this.#typeName = itemType
} else if (itemType !== this.#typeName) {
throw new TypeError(
`Queue<${this.#typeName}>: нельзя добавить ${itemType}`
)
}
this.#items.push(item)
return this
}
dequeue() {
return this.#items.shift()
}
front() {
return this.#items[0]
}
get size() { return this.#items.length }
get isEmpty() { return this.#items.length === 0 }
get type() { return this.#typeName ?? 'unknown' }
}
// Pair<A, B> — пара из двух возможно разных типов
class Pair {
constructor(first, second) {
this.first = first
this.second = second
this.firstType = typeof first
this.secondType = typeof second
}
swap() {
return new Pair(this.second, this.first)
}
map(fnFirst, fnSecond) {
return new Pair(fnFirst(this.first), fnSecond(this.second))
}
toString() {
return `Pair<${this.firstType}, ${this.secondType}>(${this.first}, ${this.second})`
}
}
// --- Демонстрация ---
console.log('=== Stack<number> ===')
const numStack = new Stack()
numStack.push(10).push(20).push(30)
console.log(numStack.toString()) // Stack<number>([10, 20, 30])
console.log(`peek: ${numStack.peek()}`) // 30
console.log(`pop: ${numStack.pop()}`) // 30
console.log(`size: ${numStack.size}`) // 2
try {
numStack.push('hello') // TypeError: Stack<number> нельзя добавить string
} catch (e) {
console.log(`Ошибка типа: ${e.message}`)
}
console.log('\n=== Queue<string> ===')
const strQueue = new Queue()
strQueue.enqueue('first').enqueue('second').enqueue('third')
console.log(`front: ${strQueue.front()}`) // 'first'
console.log(`dequeue: ${strQueue.dequeue()}`) // 'first'
console.log(`size: ${strQueue.size}`) // 2
console.log(`type: ${strQueue.type}`) // 'string'
console.log('\n=== Pair<string, number> ===')
const p = new Pair('hello', 42)
console.log(p.toString()) // Pair<string, number>(hello, 42)
const swapped = p.swap()
console.log(swapped.toString()) // Pair<number, string>(42, hello)
const mapped = p.map(s => s.toUpperCase(), n => n * 2)
console.log(mapped.toString()) // Pair<string, number>(HELLO, 84)
console.log('\n=== Практичный пример: очередь задач ===')
const taskQueue = new Queue()
taskQueue
.enqueue('fetch-users')
.enqueue('process-data')
.enqueue('send-report')
while (!taskQueue.isEmpty) {
const task = taskQueue.dequeue()
console.log(`Выполняю задачу: ${task}`)
}Без generics приходится выбирать: писать код под конкретный тип или терять типобезопасность через any:
// Проблема: функция работает только со string
function firstString(arr: string[]): string {
return arr[0]
}
// Антипаттерн: теряем типобезопасность
function firstAny(arr: any[]): any {
return arr[0] // возвращает any — TS не знает тип
}
// Решение: generic — T подставляется при вызове
function first<T>(arr: T[]): T {
return arr[0]
}
const s = first(['a', 'b', 'c']) // T = string, возвращает string
const n = first([1, 2, 3]) // T = number, возвращает numberGenerics = пишем код один раз, работает с любым типом **без потери типобезопасности**.
// Generic функция
function identity<T>(arg: T): T {
return arg
}
// Явное указание типа
const result1 = identity<string>('hello') // T = string
// Вывод типа (type inference) — TypeScript сам определяет T
const result2 = identity(42) // T = number (автоматически)// Generic интерфейс
interface Repository<T> {
findById(id: number): T | undefined
findAll(): T[]
save(item: T): void
}
// Generic класс
class Stack<T> {
private items: T[] = []
push(item: T): void {
this.items.push(item)
}
pop(): T | undefined {
return this.items.pop()
}
peek(): T | undefined {
return this.items[this.items.length - 1]
}
get size(): number {
return this.items.length
}
}
const numStack = new Stack<number>()
numStack.push(1)
numStack.push(2)
console.log(numStack.pop()) // 2 — TypeScript знает что это numberИногда нужно гарантировать что T имеет определённые свойства:
// T должен иметь поле length
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b
}
longest('hello', 'hi') // OK — string имеет length
longest([1, 2, 3], [4, 5]) // OK — array имеет length
// longest(1, 2) // Ошибка TS: number не имеет length
// keyof — ограничение ключами объекта
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: 'Алексей', age: 30 }
getProperty(user, 'name') // string
getProperty(user, 'age') // number
// getProperty(user, 'email') // Ошибка TS: 'email' не существуетfunction zip<T, U>(arr1: T[], arr2: U[]): [T, U][] {
return arr1.map((item, i) => [item, arr2[i]])
}
zip([1, 2, 3], ['a', 'b', 'c'])
// [[1, 'a'], [2, 'b'], [3, 'c']]
// Pair с двумя разными типами
class Pair<A, B> {
constructor(public first: A, public second: B) {}
swap(): Pair<B, A> {
return new Pair(this.second, this.first)
}
}// T = string если не указан явно
interface ApiResponse<T = string> {
data: T
status: number
}
const r1: ApiResponse = { data: 'hello', status: 200 } // T = string
const r2: ApiResponse<number[]> = { data: [1, 2], status: 200 } // T = number[]// Async wrapper — возвращает [data, error]
async function tryCatch<T>(
promise: Promise<T>
): Promise<[T | null, Error | null]> {
try {
const data = await promise
return [data, null]
} catch (err) {
return [null, err as Error]
}
}
const [data, error] = await tryCatch(fetch('/api/users'))
if (error) {
console.error(error.message)
} else {
console.log(data) // TypeScript знает что это Response
}Generic-стиль структуры данных: Stack, Queue, Pair с runtime type tracking
// В TypeScript это было бы Stack<T>, Queue<T>, Pair<A,B>
// В JS эмулируем через runtime проверку типов
class Stack {
#items = []
#typeName = null // запоминаем тип первого элемента
push(item) {
const itemType = Array.isArray(item) ? 'array' : typeof item
if (this.#typeName === null) {
this.#typeName = itemType // определяем тип по первому элементу
} else if (itemType !== this.#typeName) {
throw new TypeError(
`Stack<${this.#typeName}>: нельзя добавить ${itemType}`
)
}
this.#items.push(item)
return this
}
pop() {
if (this.#items.length === 0) return undefined
return this.#items.pop()
}
peek() {
return this.#items[this.#items.length - 1]
}
get size() { return this.#items.length }
get isEmpty() { return this.#items.length === 0 }
get type() { return this.#typeName ?? 'unknown' }
toString() {
return `Stack<${this.type}>([${this.#items.join(', ')}])`
}
}
class Queue {
#items = []
#typeName = null
enqueue(item) {
const itemType = Array.isArray(item) ? 'array' : typeof item
if (this.#typeName === null) {
this.#typeName = itemType
} else if (itemType !== this.#typeName) {
throw new TypeError(
`Queue<${this.#typeName}>: нельзя добавить ${itemType}`
)
}
this.#items.push(item)
return this
}
dequeue() {
return this.#items.shift()
}
front() {
return this.#items[0]
}
get size() { return this.#items.length }
get isEmpty() { return this.#items.length === 0 }
get type() { return this.#typeName ?? 'unknown' }
}
// Pair<A, B> — пара из двух возможно разных типов
class Pair {
constructor(first, second) {
this.first = first
this.second = second
this.firstType = typeof first
this.secondType = typeof second
}
swap() {
return new Pair(this.second, this.first)
}
map(fnFirst, fnSecond) {
return new Pair(fnFirst(this.first), fnSecond(this.second))
}
toString() {
return `Pair<${this.firstType}, ${this.secondType}>(${this.first}, ${this.second})`
}
}
// --- Демонстрация ---
console.log('=== Stack<number> ===')
const numStack = new Stack()
numStack.push(10).push(20).push(30)
console.log(numStack.toString()) // Stack<number>([10, 20, 30])
console.log(`peek: ${numStack.peek()}`) // 30
console.log(`pop: ${numStack.pop()}`) // 30
console.log(`size: ${numStack.size}`) // 2
try {
numStack.push('hello') // TypeError: Stack<number> нельзя добавить string
} catch (e) {
console.log(`Ошибка типа: ${e.message}`)
}
console.log('\n=== Queue<string> ===')
const strQueue = new Queue()
strQueue.enqueue('first').enqueue('second').enqueue('third')
console.log(`front: ${strQueue.front()}`) // 'first'
console.log(`dequeue: ${strQueue.dequeue()}`) // 'first'
console.log(`size: ${strQueue.size}`) // 2
console.log(`type: ${strQueue.type}`) // 'string'
console.log('\n=== Pair<string, number> ===')
const p = new Pair('hello', 42)
console.log(p.toString()) // Pair<string, number>(hello, 42)
const swapped = p.swap()
console.log(swapped.toString()) // Pair<number, string>(42, hello)
const mapped = p.map(s => s.toUpperCase(), n => n * 2)
console.log(mapped.toString()) // Pair<string, number>(HELLO, 84)
console.log('\n=== Практичный пример: очередь задач ===')
const taskQueue = new Queue()
taskQueue
.enqueue('fetch-users')
.enqueue('process-data')
.enqueue('send-report')
while (!taskQueue.isEmpty) {
const task = taskQueue.dequeue()
console.log(`Выполняю задачу: ${task}`)
}Реализуй паттерн `Result<T, E>` в JavaScript: функция `ok(value)` возвращает объект `{success: true, value}`, функция `err(error)` возвращает `{success: false, error}`. Реализуй функцию `map(result, fn)` — применяет fn к value если success === true, иначе пробрасывает ошибку без изменений. Реализуй `getOrElse(result, defaultValue)` — возвращает value если success, иначе defaultValue.
ok и err просто возвращают объекты. В map: if (result.success) return ok(fn(result.value)); else return result. В getOrElse: return result.success ? result.value : defaultValue.
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке