← Курс/Extract, Exclude и NonNullable#182 из 257+25 XP

Extract, Exclude и NonNullable

Основы: работа с union types

Extract, Exclude и NonNullable — стандартные утилиты для работы с union types. Все они реализованы через дистрибутивные conditional types.

Exclude<T, U> — убираем из union

Exclude<T, U> убирает из T все типы, совместимые с U:

type Status = 'pending' | 'active' | 'blocked' | 'deleted'

type ActiveStatus = Exclude<Status, 'deleted' | 'blocked'>
// 'pending' | 'active'

// Реализация:
type Exclude<T, U> = T extends U ? never : T
// Для каждого члена T: если совместим с U — заменяем на never
// never в union исчезает

type NoFunctions<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K]
}

Extract<T, U> — оставляем только совпадающие

Extract<T, U> оставляет из T только типы, совместимые с U:

type Events = 'click' | 'mouseover' | 'keydown' | 'keyup' | 'scroll'

type MouseEvents = Extract<Events, 'click' | 'mouseover' | `mouse${string}`>
// 'click' | 'mouseover'

type KeyEvents = Extract<Events, `key${string}`>
// 'keydown' | 'keyup'

// Реализация:
type Extract<T, U> = T extends U ? T : never

// Практика: получить общие ключи двух типов
type CommonKeys<A, B> = Extract<keyof A, keyof B>

interface User    { id: number; name: string; email: string }
interface Profile { id: number; name: string; avatar: string }

type Shared = CommonKeys<User, Profile>  // 'id' | 'name'

NonNullable<T> — убираем null и undefined

type MaybeString = string | null | undefined
type DefiniteString = NonNullable<MaybeString>  // string

// Реализация:
type NonNullable<T> = T extends null | undefined ? never : T
// Или: type NonNullable<T> = T & {}

// Практика: сделать все поля объекта non-nullable
type Required<T> = { [K in keyof T]-?: NonNullable<T[K]> }

Практические паттерны

// 1. Фильтрация ключей по типу значения
type KeysOfType<T, V> = {
  [K in keyof T]: T[K] extends V ? K : never
}[keyof T]

interface Form {
  name: string
  age: number
  email: string
  active: boolean
  role: 'admin' | 'user'
}

type StringFields  = KeysOfType<Form, string>   // 'name' | 'email'
type NumberFields  = KeysOfType<Form, number>   // 'age'
type BooleanFields = KeysOfType<Form, boolean>  // 'active'

// 2. Строить типы из кусков union
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
type SafeMethod    = Extract<HttpMethod, 'GET' | 'HEAD' | 'OPTIONS'>  // 'GET'
type MutatingMethod = Exclude<HttpMethod, 'GET'>  // 'POST' | 'PUT' | 'DELETE' | 'PATCH'

// 3. Комбинация с ReturnType
function fetchUser() { return { id: 1, name: 'Алексей', deletedAt: null as Date | null } }
type FetchResult    = ReturnType<typeof fetchUser>
type DefinedFetchResult = { [K in keyof FetchResult]: NonNullable<FetchResult[K]> }

Кастомные утилиты на основе Extract/Exclude

// PickByValue — как Pick, но по типу значения
type PickByValue<T, V> = Pick<T, KeysOfType<T, V>>

// OmitByValue — как Omit, но по типу значения
type OmitByValue<T, V> = Pick<T, Exclude<keyof T, KeysOfType<T, V>>>

interface User {
  id: number
  name: string
  email: string
  age: number
  active: boolean
}

type StringProps = PickByValue<User, string>  // { name: string; email: string }
type NonStrings  = OmitByValue<User, string>  // { id: number; age: number; active: boolean }

Примеры

Runtime аналоги Extract, Exclude, NonNullable: фильтрация массивов значений и ключей объектов

// TypeScript Extract/Exclude работают с типами.
// В JS реализуем аналогичные операции для значений и объектов.

// Аналог Exclude<T, U>: убрать из массива элементы, входящие в другой массив
function exclude(values, toRemove) {
  const removeSet = new Set(toRemove)
  return values.filter(v => !removeSet.has(v))
}

// Аналог Extract<T, U>: оставить только элементы, входящие в другой массив
function extract(values, toKeep) {
  const keepSet = new Set(toKeep)
  return values.filter(v => keepSet.has(v))
}

// Аналог NonNullable<T>: убрать null и undefined
function nonNullable(values) {
  return values.filter(v => v != null)
}

// Аналог KeysOfType<T, V>: получить ключи объекта с определённым типом значения
function keysOfType(obj, typeCheck) {
  return Object.keys(obj).filter(key => typeCheck(obj[key]))
}

// Аналог PickByValue<T, V>: взять только поля с определённым типом значения
function pickByValue(obj, typeCheck) {
  return Object.fromEntries(
    Object.entries(obj).filter(([, v]) => typeCheck(v))
  )
}

// Аналог OmitByValue<T, V>: убрать поля с определённым типом значения
function omitByValue(obj, typeCheck) {
  return Object.fromEntries(
    Object.entries(obj).filter(([, v]) => !typeCheck(v))
  )
}

// CommonKeys между двумя объектами (Extract<keyof A, keyof B>)
function commonKeys(objA, objB) {
  const keysB = new Set(Object.keys(objB))
  return Object.keys(objA).filter(k => keysB.has(k))
}

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

const allStatuses = ['pending', 'active', 'blocked', 'deleted', 'suspended']

console.log('=== Exclude ===')
const activeStatuses = exclude(allStatuses, ['deleted', 'blocked', 'suspended'])
console.log('Активные статусы:', activeStatuses)  // ['pending', 'active']

const allEvents = ['click', 'mouseover', 'keydown', 'keyup', 'scroll', 'mouseout']
const mouseEvents = extract(allEvents, ['click', 'mouseover', 'mouseout', 'mouseenter'])
console.log('\n=== Extract ===')
console.log('Mouse события:', mouseEvents)  // ['click', 'mouseover', 'mouseout']

const keyEvents = allEvents.filter(e => e.startsWith('key'))
console.log('Key события:', keyEvents)  // ['keydown', 'keyup']

console.log('\n=== NonNullable ===')
const mixed = [1, null, 'hello', undefined, false, 0, '', null, 42]
const defined = nonNullable(mixed)
console.log('Без null/undefined:', defined)  // [1, 'hello', false, 0, '', 42]

console.log('\n=== KeysOfType ===')
const formData = {
  name:    'Алексей',
  age:     30,
  email:   'alex@mail.ru',
  active:  true,
  score:   98.5,
  role:    'admin',
}

const stringKeys  = keysOfType(formData, v => typeof v === 'string')
const numberKeys  = keysOfType(formData, v => typeof v === 'number')
const booleanKeys = keysOfType(formData, v => typeof v === 'boolean')

console.log('Строковые поля:', stringKeys)   // ['name', 'email', 'role']
console.log('Числовые поля:', numberKeys)    // ['age', 'score']
console.log('Булевы поля:', booleanKeys)     // ['active']

console.log('\n=== PickByValue / OmitByValue ===')
const stringOnly = pickByValue(formData, v => typeof v === 'string')
console.log('Только строки:', stringOnly)

const noStrings = omitByValue(formData, v => typeof v === 'string')
console.log('Без строк:', noStrings)

console.log('\n=== CommonKeys ===')
const user    = { id: 1, name: 'Алексей', email: 'a@b.ru', role: 'user' }
const profile = { id: 1, name: 'Алексей', avatar: 'img.png', bio: 'Dev' }

const shared = commonKeys(user, profile)
console.log('Общие ключи:', shared)  // ['id', 'name']