← Курс/Mapped Types (отображённые типы)#173 из 257+35 XP

Mapped Types — отображённые типы

Синтаксис: `{ [K in keyof T]: ... }`

Mapped types позволяют создавать новые типы путём итерации по ключам существующего типа — как map() для массивов, но для типов:

// Как Partial<T> реализован в lib.d.ts:
type Partial<T> = {
  [K in keyof T]?: T[K]
}

// Как Readonly<T> реализован в lib.d.ts:
type Readonly<T> = {
  readonly [K in keyof T]: T[K]
}

interface User {
  name: string
  age: number
  email: string
}

type PartialUser = Partial<User>
// { name?: string; age?: number; email?: string }

type ReadonlyUser = Readonly<User>
// { readonly name: string; readonly age: number; readonly email: string }

Модификаторы: +/- для readonly и ?

Плюс добавляет модификатор, минус убирает:

// Required<T> — убирает опциональность (минус у ?)
type Required<T> = {
  [K in keyof T]-?: T[K]
}

// Mutable<T> — убирает readonly (минус у readonly)
type Mutable<T> = {
  -readonly [K in keyof T]: T[K]
}

interface Config {
  readonly host: string
  port?: number
}

type MutableConfig = Mutable<Config>
// { host: string; port?: number }  — readonly убран

type RequiredConfig = Required<Config>
// { readonly host: string; port: number }  — ? убран

Template Literal Types

Позволяют строить типы строк с шаблонами:

type EventName = `on${Capitalize<string>}`
// 'onClick', 'onChange', 'onSubmit', ...

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

interface User { name: string; age: number }

type UserGetters = Getters<User>
// { getName: () => string; getAge: () => number }

Переименование ключей через `as`

// EventMap — создаёт обработчики событий для каждого поля
type EventMap<T> = {
  [K in keyof T as `on${Capitalize<string & K>}Change`]: (value: T[K]) => void
}

interface Form { username: string; password: string }

type FormEvents = EventMap<Form>
// {
//   onUsernameChange: (value: string) => void
//   onPasswordChange: (value: string) => void
// }

Nullable<T> и другие utility types через mapped types

// Делает все поля nullable
type Nullable<T> = {
  [K in keyof T]: T[K] | null
}

// Только указанные ключи опциональны
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>

// Все значения — строки (для форм)
type Stringify<T> = {
  [K in keyof T]: string
}

// keyof и typeof вместе
const palette = { red: '#ff0000', green: '#00ff00', blue: '#0000ff' }
type ColorKey = keyof typeof palette  // 'red' | 'green' | 'blue'

Примеры

makeGetters и makeProxy — mapped-type-like паттерны в JS: автогенерация методов для полей объекта

// Mapped types в TypeScript генерируют новые типы из существующих.
// В JavaScript мы реализуем те же паттерны динамически в runtime.

// makeGetters — создаёт объект с методами getX() для каждого поля
function makeGetters(obj) {
  const getters = {}
  Object.keys(obj).forEach(key => {
    const getterName = 'get' + key[0].toUpperCase() + key.slice(1)
    getters[getterName] = () => obj[key]
  })
  return getters
}

// makeGettersAndSetters — геттеры и сеттеры через замыкания
function makeGettersAndSetters(initialObj) {
  const state = Object.assign({}, initialObj)
  const api = {}

  Object.keys(state).forEach(key => {
    const capitalized = key[0].toUpperCase() + key.slice(1)
    api['get' + capitalized] = () => state[key]
    api['set' + capitalized] = (value) => { state[key] = value }
  })

  return api
}

// makeProxy — Proxy с логированием (как mapped type + логика)
function makeProxy(obj) {
  return new Proxy(obj, {
    get(target, prop) {
      console.log(`GET ${String(prop)}: ${JSON.stringify(target[prop])}`)
      return target[prop]
    },
    set(target, prop, value) {
      console.log(`SET ${String(prop)}: ${JSON.stringify(target[prop])} -> ${JSON.stringify(value)}`)
      target[prop] = value
      return true
    }
  })
}

// makeEventMap — создаёт обработчики onXChange для каждого поля
// (аналог EventMap<T> из TypeScript)
function makeEventMap(obj) {
  const handlers = {}
  const listeners = {}

  Object.keys(obj).forEach(key => {
    const eventName = 'on' + key[0].toUpperCase() + key.slice(1) + 'Change'
    listeners[key] = []
    handlers[eventName] = (callback) => {
      listeners[key].push(callback)
    }
  })

  // Обёртка над объектом: при изменении вызывает слушателей
  const proxy = new Proxy(obj, {
    set(target, prop, value) {
      const old = target[prop]
      target[prop] = value
      if (prop in listeners) {
        listeners[prop].forEach(cb => cb(value, old))
      }
      return true
    }
  })

  return { proxy, handlers }
}

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

const user = { name: 'Алексей', age: 30, email: 'alex@example.com' }

console.log('=== makeGetters ===')
const getters = makeGetters(user)
console.log(getters.getName())   // 'Алексей'
console.log(getters.getAge())    // 30
console.log(getters.getEmail())  // 'alex@example.com'

console.log('\n=== makeGettersAndSetters ===')
const store = makeGettersAndSetters({ name: 'Иван', score: 100 })
console.log(store.getName())    // 'Иван'
store.setName('Пётр')
console.log(store.getName())    // 'Пётр'
store.setScore(store.getScore() + 50)
console.log(store.getScore())   // 150

console.log('\n=== makeEventMap ===')
const { proxy: form, handlers } = makeEventMap({ username: '', email: '' })
handlers.onUsernameChange((newVal) => console.log('username изменился на:', newVal))
handlers.onEmailChange((newVal) => console.log('email изменился на:', newVal))
form.username = 'john_doe'   // username изменился на: john_doe
form.email = 'john@test.com' // email изменился на: john@test.com