TypeScript использует **структурную типизацию** — два типа совместимы, если у них одинаковая структура. Но иногда это приводит к логическим ошибкам:
type UserId = string
type OrderId = string
function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }
const userId: UserId = 'user-123'
const orderId: OrderId = 'order-456'
getUser(orderId) // TypeScript не выдаёт ошибку! string совместим с string
getOrder(userId) // Тоже молча принимает — это баг!Добавляем уникальный «бренд» — фантомное поле, которое существует только на уровне типов:
// Паттерн 1: через intersection с object
type Brand<T, B extends string> = T & { readonly __brand: B }
type UserId = Brand<string, 'UserId'>
type OrderId = Brand<string, 'OrderId'>
function getUser(id: UserId) { /* ... */ }
const userId = 'user-123' as UserId // type assertion — точка входа
const orderId = 'order-456' as OrderId
getUser(userId) // OK
// getUser(orderId) // Ошибка TS! OrderId ≠ UserId
// getUser('raw') // Ошибка TS! string ≠ UserId// Создаём «умные конструкторы» с валидацией
type Email = Brand<string, 'Email'>
type PositiveNumber = Brand<number, 'PositiveNumber'>
function createEmail(raw: string): Email {
if (!/S+@S+.S+/.test(raw)) {
throw new Error(`Невалидный email: ${raw}`)
}
return raw as Email
}
function createPositive(n: number): PositiveNumber {
if (n <= 0) throw new RangeError(`Должно быть положительным: ${n}`)
return n as PositiveNumber
}
function sendEmail(to: Email, subject: string) { /* ... */ }
const email = createEmail('user@example.com')
sendEmail(email, 'Привет!')
// sendEmail('raw-string', 'Привет!') // Ошибка TS!declare const __brand: unique symbol
type Opaque<T, B> = T & { readonly [__brand]: B }
type Meters = Opaque<number, 'Meters'>
type Kilograms = Opaque<number, 'Kilograms'>
function toMeters(n: number): Meters { return n as Meters }
function toKg(n: number): Kilograms { return n as Kilograms }
function calculateBMI(weight: Kilograms, height: Meters): number {
return weight / (height * height)
}
const weight = toKg(70)
const height = toMeters(1.75)
calculateBMI(weight, height) // OK
// calculateBMI(height, weight) // Ошибка! Аргументы перепутаны// 1. Идентификаторы разных сущностей
type UserId = Brand<number, 'UserId'>
type ProductId = Brand<number, 'ProductId'>
type CartId = Brand<number, 'CartId'>
// 2. Валютные суммы
type USD = Brand<number, 'USD'>
type EUR = Brand<number, 'EUR'>
type RUB = Brand<number, 'RUB'>
// Нельзя случайно сложить доллары с рублями
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD
}
// 3. Строки прошедшие санитизацию
type SafeHtml = Brand<string, 'SafeHtml'>
type RawInput = Brand<string, 'RawInput'>
function sanitize(input: RawInput): SafeHtml { /* ... */ }
function renderHtml(html: SafeHtml): void { /* ... */ }Runtime реализация брендирования: умные конструкторы с валидацией, защита от смешивания несовместимых значений
// В TypeScript branded types проверяются только компилятором.
// В JavaScript реализуем runtime-версию через Symbol-бренды.
// Фабрика брендированных типов
function createBrand(brandName) {
const BRAND = Symbol(`Brand:${brandName}`)
function brand(value) {
if (value === null || (typeof value !== 'string' && typeof value !== 'number')) {
throw new TypeError(`Невалидное значение для ${brandName}`)
}
// Создаём объект-обёртку с брендом (только для примера в JS)
// В TypeScript это просто type assertion, без runtime-объекта
return Object.freeze({ value, [BRAND]: true, type: brandName })
}
brand.is = (x) => x && typeof x === 'object' && x[BRAND] === true
brand.brandName = brandName
return brand
}
// Создаём брендированные типы
const UserId = createBrand('UserId')
const OrderId = createBrand('OrderId')
const Email = createBrand('Email')
// Умные конструкторы с валидацией
function createEmail(raw) {
if (!/S+@S+.S+/.test(raw)) {
throw new Error(`Невалидный email: "${raw}"`)
}
return Email(raw)
}
function createUserId(id) {
if (typeof id !== 'number' || id <= 0 || !Number.isInteger(id)) {
throw new Error(`UserId должен быть положительным целым: ${id}`)
}
return UserId(id)
}
function createOrderId(id) {
if (typeof id !== 'number' || id <= 0 || !Number.isInteger(id)) {
throw new Error(`OrderId должен быть положительным целым: ${id}`)
}
return OrderId(id)
}
// Функции, принимающие строго брендированные типы
function getUser(id) {
if (!UserId.is(id)) throw new TypeError(`getUser: ожидается UserId, получен ${id?.type}`)
return { id: id.value, name: `Пользователь #${id.value}` }
}
function getOrder(id) {
if (!OrderId.is(id)) throw new TypeError(`getOrder: ожидается OrderId, получен ${id?.type}`)
return { id: id.value, total: id.value * 100 }
}
function sendEmail(to, subject) {
if (!Email.is(to)) throw new TypeError(`sendEmail: ожидается Email, получен тип ${typeof to}`)
console.log(`Отправка письма на ${to.value}: ${subject}`)
}
// --- Демонстрация ---
console.log('=== Создание брендированных значений ===')
const userId = createUserId(42)
const orderId = createOrderId(99)
const email = createEmail('user@example.com')
console.log('userId:', userId)
console.log('orderId:', orderId)
console.log('email:', email)
console.log('\n=== Правильное использование ===')
const user = getUser(userId)
const order = getOrder(orderId)
console.log('User:', user)
console.log('Order:', order)
sendEmail(email, 'Подтверждение заказа')
console.log('\n=== Защита от смешивания (runtime) ===')
try {
getUser(orderId) // передаём OrderId вместо UserId
} catch (e) {
console.log('Ошибка:', e.message)
}
try {
getOrder(userId) // передаём UserId вместо OrderId
} catch (e) {
console.log('Ошибка:', e.message)
}
try {
sendEmail('raw-string@mail.ru', 'тест') // небрендированная строка
} catch (e) {
console.log('Ошибка:', e.message)
}
console.log('\n=== Умные конструкторы с валидацией ===')
try {
createEmail('не-email')
} catch (e) {
console.log('Ошибка email:', e.message)
}
try {
createUserId(-5)
} catch (e) {
console.log('Ошибка userId:', e.message)
}TypeScript использует **структурную типизацию** — два типа совместимы, если у них одинаковая структура. Но иногда это приводит к логическим ошибкам:
type UserId = string
type OrderId = string
function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }
const userId: UserId = 'user-123'
const orderId: OrderId = 'order-456'
getUser(orderId) // TypeScript не выдаёт ошибку! string совместим с string
getOrder(userId) // Тоже молча принимает — это баг!Добавляем уникальный «бренд» — фантомное поле, которое существует только на уровне типов:
// Паттерн 1: через intersection с object
type Brand<T, B extends string> = T & { readonly __brand: B }
type UserId = Brand<string, 'UserId'>
type OrderId = Brand<string, 'OrderId'>
function getUser(id: UserId) { /* ... */ }
const userId = 'user-123' as UserId // type assertion — точка входа
const orderId = 'order-456' as OrderId
getUser(userId) // OK
// getUser(orderId) // Ошибка TS! OrderId ≠ UserId
// getUser('raw') // Ошибка TS! string ≠ UserId// Создаём «умные конструкторы» с валидацией
type Email = Brand<string, 'Email'>
type PositiveNumber = Brand<number, 'PositiveNumber'>
function createEmail(raw: string): Email {
if (!/S+@S+.S+/.test(raw)) {
throw new Error(`Невалидный email: ${raw}`)
}
return raw as Email
}
function createPositive(n: number): PositiveNumber {
if (n <= 0) throw new RangeError(`Должно быть положительным: ${n}`)
return n as PositiveNumber
}
function sendEmail(to: Email, subject: string) { /* ... */ }
const email = createEmail('user@example.com')
sendEmail(email, 'Привет!')
// sendEmail('raw-string', 'Привет!') // Ошибка TS!declare const __brand: unique symbol
type Opaque<T, B> = T & { readonly [__brand]: B }
type Meters = Opaque<number, 'Meters'>
type Kilograms = Opaque<number, 'Kilograms'>
function toMeters(n: number): Meters { return n as Meters }
function toKg(n: number): Kilograms { return n as Kilograms }
function calculateBMI(weight: Kilograms, height: Meters): number {
return weight / (height * height)
}
const weight = toKg(70)
const height = toMeters(1.75)
calculateBMI(weight, height) // OK
// calculateBMI(height, weight) // Ошибка! Аргументы перепутаны// 1. Идентификаторы разных сущностей
type UserId = Brand<number, 'UserId'>
type ProductId = Brand<number, 'ProductId'>
type CartId = Brand<number, 'CartId'>
// 2. Валютные суммы
type USD = Brand<number, 'USD'>
type EUR = Brand<number, 'EUR'>
type RUB = Brand<number, 'RUB'>
// Нельзя случайно сложить доллары с рублями
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD
}
// 3. Строки прошедшие санитизацию
type SafeHtml = Brand<string, 'SafeHtml'>
type RawInput = Brand<string, 'RawInput'>
function sanitize(input: RawInput): SafeHtml { /* ... */ }
function renderHtml(html: SafeHtml): void { /* ... */ }Runtime реализация брендирования: умные конструкторы с валидацией, защита от смешивания несовместимых значений
// В TypeScript branded types проверяются только компилятором.
// В JavaScript реализуем runtime-версию через Symbol-бренды.
// Фабрика брендированных типов
function createBrand(brandName) {
const BRAND = Symbol(`Brand:${brandName}`)
function brand(value) {
if (value === null || (typeof value !== 'string' && typeof value !== 'number')) {
throw new TypeError(`Невалидное значение для ${brandName}`)
}
// Создаём объект-обёртку с брендом (только для примера в JS)
// В TypeScript это просто type assertion, без runtime-объекта
return Object.freeze({ value, [BRAND]: true, type: brandName })
}
brand.is = (x) => x && typeof x === 'object' && x[BRAND] === true
brand.brandName = brandName
return brand
}
// Создаём брендированные типы
const UserId = createBrand('UserId')
const OrderId = createBrand('OrderId')
const Email = createBrand('Email')
// Умные конструкторы с валидацией
function createEmail(raw) {
if (!/S+@S+.S+/.test(raw)) {
throw new Error(`Невалидный email: "${raw}"`)
}
return Email(raw)
}
function createUserId(id) {
if (typeof id !== 'number' || id <= 0 || !Number.isInteger(id)) {
throw new Error(`UserId должен быть положительным целым: ${id}`)
}
return UserId(id)
}
function createOrderId(id) {
if (typeof id !== 'number' || id <= 0 || !Number.isInteger(id)) {
throw new Error(`OrderId должен быть положительным целым: ${id}`)
}
return OrderId(id)
}
// Функции, принимающие строго брендированные типы
function getUser(id) {
if (!UserId.is(id)) throw new TypeError(`getUser: ожидается UserId, получен ${id?.type}`)
return { id: id.value, name: `Пользователь #${id.value}` }
}
function getOrder(id) {
if (!OrderId.is(id)) throw new TypeError(`getOrder: ожидается OrderId, получен ${id?.type}`)
return { id: id.value, total: id.value * 100 }
}
function sendEmail(to, subject) {
if (!Email.is(to)) throw new TypeError(`sendEmail: ожидается Email, получен тип ${typeof to}`)
console.log(`Отправка письма на ${to.value}: ${subject}`)
}
// --- Демонстрация ---
console.log('=== Создание брендированных значений ===')
const userId = createUserId(42)
const orderId = createOrderId(99)
const email = createEmail('user@example.com')
console.log('userId:', userId)
console.log('orderId:', orderId)
console.log('email:', email)
console.log('\n=== Правильное использование ===')
const user = getUser(userId)
const order = getOrder(orderId)
console.log('User:', user)
console.log('Order:', order)
sendEmail(email, 'Подтверждение заказа')
console.log('\n=== Защита от смешивания (runtime) ===')
try {
getUser(orderId) // передаём OrderId вместо UserId
} catch (e) {
console.log('Ошибка:', e.message)
}
try {
getOrder(userId) // передаём UserId вместо OrderId
} catch (e) {
console.log('Ошибка:', e.message)
}
try {
sendEmail('raw-string@mail.ru', 'тест') // небрендированная строка
} catch (e) {
console.log('Ошибка:', e.message)
}
console.log('\n=== Умные конструкторы с валидацией ===')
try {
createEmail('не-email')
} catch (e) {
console.log('Ошибка email:', e.message)
}
try {
createUserId(-5)
} catch (e) {
console.log('Ошибка userId:', e.message)
}Реализуй умные конструкторы: `createPositiveInt(n)` — проверяет что n целое и > 0, бросает RangeError иначе; `createNonEmptyString(s)` — проверяет что s строка и не пустая (после trim), бросает Error иначе; `createPercentage(n)` — проверяет что n число от 0 до 100 включительно, бросает RangeError иначе. Реализуй функцию `applyDiscount(price, discountPercent)` которая использует эти конструкторы для валидации аргументов перед вычислением скидки.
createPositiveInt: if (!Number.isInteger(n) || n <= 0) throw new RangeError(...). createNonEmptyString: if (typeof s !== "string" || s.trim() === "") throw new Error(...); return s.trim(). createPercentage: if (typeof n !== "number" || n < 0 || n > 100) throw new RangeError(...).
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке