← Курс/Type Predicates: x is T#177 из 257+25 XP

Type Predicates: x is T

Проблема: TypeScript не знает тип после проверки

function isString(value: unknown): boolean {
  return typeof value === 'string'
}

function processValue(value: string | number) {
  if (isString(value)) {
    // TypeScript всё ещё считает value: string | number
    value.toUpperCase()  // Ошибка TS! TypeScript не «помнит» что мы проверили
  }
}

Проблема: вынос проверки в функцию «стирает» информацию о типе для TypeScript.

Решение: type predicate «param is Type»

// Возвращаемый тип — predicate: «если функция вернула true, то value — это string»
function isString(value: unknown): value is string {
  return typeof value === 'string'
}

function processValue(value: string | number) {
  if (isString(value)) {
    value.toUpperCase()  // OK! TypeScript знает: value — string
  } else {
    value.toFixed(2)     // OK! TypeScript знает: value — number
  }
}

User-defined type guards для объектов

interface Cat { meow(): void; purr(): void }
interface Dog { bark(): void; fetch(): void }

function isCat(animal: Cat | Dog): animal is Cat {
  return 'meow' in animal
}

function makeNoise(animal: Cat | Dog) {
  if (isCat(animal)) {
    animal.meow()    // animal is Cat — TS знает
    animal.purr()
  } else {
    animal.bark()    // animal is Dog
  }
}

asserts — функции, гарантирующие тип

// Если функция не бросает — значит value является T
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new TypeError(`Ожидалась строка, получен ${typeof value}`)
  }
}

function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
  if (value == null) {
    throw new Error('Значение не должно быть null или undefined')
  }
}

const maybeString: unknown = 'hello'
assertIsString(maybeString)
maybeString.toUpperCase()  // OK — TypeScript знает что это string

Array.filter с type predicates

Без type predicate TypeScript не может сузить тип после filter:

const values: (string | null | undefined)[] = ['a', null, 'b', undefined, 'c']

// Без predicate — тип элементов всё ещё string | null | undefined
const v1 = values.filter(Boolean)  // (string | null | undefined)[]

// С predicate — TypeScript знает что отфильтрованы null/undefined
function isDefined<T>(value: T | null | undefined): value is T {
  return value != null
}

const v2 = values.filter(isDefined)  // string[] — точный тип!

Составные type guards

interface User { id: number; name: string; role: 'user' }
interface Admin { id: number; name: string; role: 'admin'; permissions: string[] }

type Person = User | Admin

function isAdmin(person: Person): person is Admin {
  return person.role === 'admin'
}

// Комбинирование
function hasPermission(person: Person, perm: string): boolean {
  return isAdmin(person) && person.permissions.includes(perm)
}

// Массив — оставить только Admin
const people: Person[] = [...]
const admins = people.filter(isAdmin)  // Admin[] — тип сужен!

Примеры

Type guard функции для runtime проверки типов: isString, isNumber, isDefined, isUser — паттерны для безопасной работы с unknown данными

// TypeScript type predicates — это обычные функции-предикаты,
// но с аннотацией которая сообщает TypeScript о сужении типа.
// В JS используем те же предикатные функции для runtime-безопасности.

// Базовые предикаты (аналог: value is string)
const isString  = (v) => typeof v === 'string'
const isNumber  = (v) => typeof v === 'number' && !isNaN(v)
const isBoolean = (v) => typeof v === 'boolean'
const isArray   = (v) => Array.isArray(v)
const isObject  = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
const isDefined = (v) => v != null  // исключает null и undefined

// Составной предикат: создаём type guard для объектов
function hasShape(value, shape) {
  if (!isObject(value)) return false
  for (const [key, validator] of Object.entries(shape)) {
    if (typeof validator === 'string') {
      if (typeof value[key] !== validator) return false
    } else if (typeof validator === 'function') {
      if (!validator(value[key])) return false
    }
  }
  return true
}

// Конкретные type guards для наших типов
const isUser = (v) => hasShape(v, {
  id:   'number',
  name: 'string',
  email: isString,
})

const isAdmin = (v) => isUser(v) && v.role === 'admin'

const isProduct = (v) => hasShape(v, {
  id:    'number',
  name:  'string',
  price: isNumber,
})

// assert — бросает если условие не выполнено
function assert(condition, message) {
  if (!condition) throw new TypeError(message)
}

function assertDefined(value, name = 'value') {
  assert(isDefined(value), `${name} не должен быть null/undefined`)
  return value
}

function assertUser(value) {
  assert(isUser(value), `Ожидается объект User: ${JSON.stringify(value)}`)
  return value
}

// filter с предикатом — аналог array.filter(isDefined) в TS
function filterDefined(arr) {
  return arr.filter(isDefined)
}

function filterByPredicate(arr, predicate) {
  return arr.filter(predicate)
}

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

console.log('=== Базовые предикаты ===')
const values = [1, 'hello', null, true, undefined, 42, '', false, {}, []]
console.log('isString:', values.filter(isString))
console.log('isNumber:', values.filter(isNumber))
console.log('isDefined:', filterDefined(values))

console.log('\n=== hasShape type guard ===')
const data = [
  { id: 1, name: 'Алексей', email: 'alex@mail.ru' },
  { id: 2, name: 'Ольга',   email: 'olga@mail.ru', role: 'admin' },
  { id: 'bad', name: 'Иван' },  // невалидный
  'just a string',
  null,
  { id: 3, name: 'Мария', email: 'maria@mail.ru' },
]

const users = filterByPredicate(data, isUser)
console.log('Валидных пользователей:', users.length)  // 3
users.forEach(u => console.log(` - ${u.name} (${u.email})`))

console.log('\n=== Array.filter с type guard ===')
const maybeUsers = [
  { id: 1, name: 'Alice', email: 'a@b.com' },
  null,
  { id: 2, name: 'Bob', email: 'b@c.com' },
  undefined,
  { id: 'bad' },
]

const validUsers = maybeUsers.filter(isUser)
console.log('Прошли фильтр:', validUsers.length)
// В TypeScript это дало бы тип User[] а не (User | null | undefined)[]

console.log('\n=== assert (не бросает — гарантирует тип) ===')
function processUser(data) {
  assertDefined(data, 'data')
  assertUser(data)
  console.log(`Обработка: ${data.name} (${data.email})`)
}

processUser({ id: 1, name: 'Иван', email: 'ivan@mail.ru' })

try {
  processUser(null)
} catch (e) {
  console.log('Ошибка null:', e.message)
}

try {
  processUser({ name: 'без id и email' })
} catch (e) {
  console.log('Ошибка невалидного:', e.message)
}