Utility types - вбудовані перетворення типів, щоб не описувати схожі форми вручну:
interface User {
id: number;
name: string;
email: string;
password: string;
}
type UserUpdate = Partial<Omit<User, 'id'>>; // усі поля, крім id, необов'язкові
type PublicUser = Omit<User, 'password'>;
type UserPreview = Pick<User, 'id' | 'name'>;
type RolesMap = Record<'admin' | 'editor', string[]>;
type Frozen = Readonly<User>;
Ще корисні: Required, NonNullable, ReturnType<typeof fn>, Parameters<typeof fn>, Awaited<Promise<T>>.
Під капотом це mapped types - тип, що проходить по ключах іншого типу:
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Модифікатори можна і додавати, і знімати
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
// Перейменування ключів через as (TS 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<Pick<User, 'name'>>; // { getName: () => string }
Навіщо на практиці: один джерельний тип (User), а похідні - форма оновлення, публічне представлення, стан форми - виводяться з нього. Додали поле в User - усі похідні оновилися, і компілятор покаже місця, які треба доробити.
Обережно: надто «розумні» типи важко читати й налагоджувати, а повідомлення про помилки в них стають незрозумілими. Якщо тип потрібно пояснювати, інколи простіше описати його явно.