← Курс/Type Assertions и satisfies#174 из 257+25 XP

Type Assertions — утверждения о типах

`value as Type` — «я знаю тип лучше TS»

Type assertion говорит TypeScript: «доверяй мне, это точно этот тип». Компилятор соглашается без проверки:

const input = document.getElementById('username') as HTMLInputElement
console.log(input.value)  // OK — TS знает что это HTMLInputElement

// Без assertion:
const raw = document.getElementById('username')
// raw: HTMLElement | null
console.log(raw.value)  // Ошибка: Property 'value' does not exist on type 'HTMLElement'

Assertions работают только если типы **частично совместимы** — нельзя привести string к number:

const x = 'hello' as number  // Ошибка: Conversion of type 'string' to type 'number' may be a mistake

Non-null assertion: `value!`

Восклицательный знак говорит TS: «это точно не null и не undefined»:

function getUser(): User | null { ... }

const user = getUser()
console.log(user!.name)  // ! — я гарантирую что user не null
// Если user всё же null — TypeError в runtime!

// Лучше использовать явную проверку:
if (user !== null) {
  console.log(user.name)  // TypeScript сам сузит тип
}

Double assertion: `value as unknown as NewType`

Когда типы полностью несовместимы, используют двойное приведение через unknown:

const x = 'hello' as unknown as number  // Компилируется, но опасно!

// Реальный случай: принудительное приведение в тестах
const mockUser = {} as unknown as User
// Используется в моках, но в продакшне — антипаттерн

Оператор `satisfies`

Появился в TypeScript 4.9. Проверяет что значение **соответствует** типу, но сохраняет **точный выведенный тип**:

const palette = {
  red: [255, 0, 0],
  green: '#00ff00',
} satisfies Record<string, string | number[]>

// palette.red — TypeScript знает что это number[], а не string | number[]
// palette.green — TypeScript знает что это string, а не string | number[]
palette.red.map(x => x * 2)    // OK — TS видит number[]
palette.green.toUpperCase()    // OK — TS видит string

// С обычной аннотацией :Record<...> информация о точных типах теряется:
const palette2: Record<string, string | number[]> = { red: [255, 0, 0] }
palette2.red.map(x => x * 2)  // Ошибка — TS думает это string | number[]

Когда assertions опасны

// ОПАСНО: assertion без реальной проверки
async function loadUser(): Promise<User> {
  const data = await fetch('/api/user').then(r => r.json())
  return data as User  // Нет гарантий что data действительно User!
}

// БЕЗОПАСНО: type guard с реальной проверкой
function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    typeof (value as any).name === 'string' &&
    typeof (value as any).age === 'number'
  )
}

async function loadUserSafe(): Promise<User | null> {
  const data = await fetch('/api/user').then(r => r.json())
  return isUser(data) ? data : null
}

**Правило:** assertions — последний выход. Предпочитай type guards, они безопаснее.

Примеры

Runtime assertions: assertType, assertNotNull и TypedStorage — безопасное vs небезопасное приведение типов

// TypeScript assertions работают только при компиляции.
// В JS реализуем runtime-версии тех же идей.

class AssertionError extends Error {
  constructor(message) {
    super(message)
    this.name = 'AssertionError'
  }
}

// assertType — runtime аналог 'value as Type' (но безопасный: с проверкой)
function assertType(value, type, message) {
  const actual = value === null ? 'null' :
                 Array.isArray(value) ? 'array' :
                 typeof value

  if (actual !== type) {
    throw new AssertionError(
      message || `Expected type "${type}", got "${actual}"`
    )
  }
  return value
}

// assertNotNull — runtime аналог non-null assertion (value!)
function assertNotNull(value, message) {
  if (value === null || value === undefined) {
    throw new AssertionError(message || `Expected non-null value, got ${value}`)
  }
  return value
}

// assertShape — runtime аналог type guard для объектов
function assertShape(value, shape) {
  if (typeof value !== 'object' || value === null) {
    throw new AssertionError(`Expected object, got ${typeof value}`)
  }
  for (const key of Object.keys(shape)) {
    const expected = shape[key]
    const actual = value[key] === null ? 'null' :
                   Array.isArray(value[key]) ? 'array' :
                   typeof value[key]
    if (actual !== expected) {
      throw new AssertionError(
        `Property "${key}": expected "${expected}", got "${actual}"`
      )
    }
  }
  return value
}

// --- Демонстрация ---

console.log('=== assertType ===')
console.log(assertType('hello', 'string'))  // 'hello'
console.log(assertType(42, 'number'))       // 42
console.log(assertType([1,2,3], 'array'))   // [1, 2, 3]

try {
  assertType('hello', 'number')
} catch (e) {
  console.log(e.name + ':', e.message)  // AssertionError: Expected type "number", got "string"
}

console.log('\n=== assertNotNull ===')
const user = { name: 'Алексей' }
console.log(assertNotNull(user).name)  // 'Алексей'

try {
  assertNotNull(null, 'User не должен быть null')
} catch (e) {
  console.log(e.message)  // 'User не должен быть null'
}

console.log('\n=== assertShape ===')
const apiResponse = { name: 'Bob', age: 25, email: 'bob@test.com' }
const validated = assertShape(apiResponse, { name: 'string', age: 'number' })
console.log('Валидный объект:', validated.name)  // 'Bob'

try {
  assertShape({ name: 42, age: 'old' }, { name: 'string', age: 'number' })
} catch (e) {
  console.log(e.message)  // 'Property "name": expected "string", got "number"'
}