Галоўная / Артыкулы / Power Tools у TypeScript: extend, infer, ключы Maps і as Remaps

Power Tools у TypeScript: extend, infer, ключы Maps і as Remaps

Створыце типы рэгламентнага рангу, сумяшчаючы умовныя выразы, адгукаванне, ціклы з каратэкстуваннем і перекаратэкстуванне ключоў — тое ж самае інструментарыя, якое викорыстоўваецца ў Zod і tRPC.

1192 слоў

Пасля кальвінаў з TypeScript, вбудованыя функцыі такія як Partial, Pick і Omit зрэшты перестаюць падходзіць для рэальных задач бібліятэк: выявлення типу значэння, яке вяртае функцыя, трансформація кожнага ключа або стварэння типаў, якія прыстосаваліся да іншых типаў. Чатыры функцыі разам дазволяюць досягнуць такага рэвалюцыі: умовны extends, infer, ціклы, якія працуюць з keyof, і переназначэння ключоў за дапамогою as. Адна-адна яны є простымі для викорыстання; разам жа яны ствараюць патэрны, якія викорыстоўваюцца ў Zod, tRPC і React Router.

1. Умовныя типы з extends

У контексте типаў extends ёсць статэмента if:

type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>;      // false

Це означае: якщо T можа быць прызначаны як string, то рэзультат — true, інакш — false.

Умовныя выразы станоць сильнейшымі, калі яны маюць дыстрыбутивную структуру. Аднасць T пераглядаецца элемент за элементам:

type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// Result = string[] | number[]   (not (string | number)[])

Самэй тым фільтрацыя аднасцей выконваецца швыта:

type ExcludeString<T> = T extends string ? never : T;
type NoStrings = ExcludeString<string | number | boolean>;
// NoStrings = number | boolean

never не з’являецца ў аднасцях — гэта тая ж ідея, якая лежыць у основе вбудованага Exclude<T, U>.

2. Выяўленне форм з адпамінанням infer

infer выкорыстоўваецца толькі ў клазу extends. Ён прызначае месца для замены, якое TypeScript запоўняе на адпаведнаю форму. Цэль — класычны выяўлення типу вярнення:

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
  return { id: 1, name: "Oussama" };
}type User = MyReturnType<typeof getUser>;
// User = { id: number; name: string }

Этапы: падбіраем typeof getUser па (...args: any[]) => infer R, прыяўляем R да рэальнага значэння, якое вяртаецца, і выкарыстоўваем яго як рэзультат.

infer таксама можа выкорыстоўваться і ў іншых месцах:

// Extract the element type of an array
type ElementOf<T> = T extends (infer U)[] ? U : never;
type Item = ElementOf<string[]>; // string
// Extract the resolved type of a Promise
type Awaited2<T> = T extends Promise<infer U> ? U : T;
type Data = Awaited2<Promise<{ status: number }>>;
// Data = { status: number }// Extract the first argument of a function
type FirstArg<T> = T extends (arg: infer A, ...rest: any[]) => any ? A : never;
type Arg = FirstArg<(id: number, name: string) => void>; // number

Рекурсія распакоўвае вкладаныя обяцанні:

type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type Flat = DeepAwaited<Promise<Promise<Promise<string>>>>;
// Flat = string

3. Пераходы ціклам з викорыстаннем типоў з мапаванням keyof

keyof вяртае аб’еднанне ключоў:

interface User {
  id: number;
  name: string;
  email: string;
}
type UserKeys = keyof User; // "id" | "name" | "email"

Тыпы з мапаванням ітеруюць гэтае аб’еднанне як у режыме for...in на рэвэлі типу:

type Readonly2<T> = {
  [K in keyof T]: T[K];
};

Дадзіце модыфікаторы для необавязковых або толькі для чытання копій:

// Make every property optional
type Optional<T> = {
  [K in keyof T]?: T[K];
};
// Make every property readonly
type ReadonlyAll<T> = {
  readonly [K in keyof T]: T[K];
};// Remove readonly / optional with a minus modifier
type Mutable<T> = {
  -readonly [K in keyof T]-?: T[K];
};

Трансфармавайце значэнні пад час ітерацый:

type Stringify<T> = {
  [K in keyof T]: string;
};
type StringifiedUser = Stringify<User>;
// { id: string; name: string; email: string }

Спяўнюйце ўмовы разам з ціклам:

type NullableStrings<T> = {
  [K in keyof T]: T[K] extends string ? T[K] | null : T[K];
};
type Result2 = NullableStrings<User>;
// { id: number; name: string | null; email: string | null }

4. Перемапаванне ключоў з викорыстаннем as

Усередине типу з мапаванням as вычысляе новую назву ключа для кожнай властівы:

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
/*
{
  getId: () => number;
  getName: () => string;
  getEmail: () => string;
}
*/

[K in keyof T] абвяртае ітерацыю; шаблонны літэрал стварае ключы у стылі getName; T[K] стае типам, які вяртае гетер.

Выкарыстоўвайце as never, каб адмахнуцца ключоў пад час ітерацый:

type OmitByType<T, U> = {
  [K in keyof T as T[K] extends U ? never : K]: T[K];
};
interface Mixed {
  id: number;
  name: string;
  active: boolean;
}type OnlyNonBoolean = OmitByType<Mixed, boolean>;
// { id: number; name: string }

Шаблон extends усередине as — гэта спосаб стварэння варіянтав Pick/Omit, базаваных на значэннях.

Усе чатыро разам

Рэалістычны камбінат: верыфікаторы для поль дадзейна, прычыненне функцыйных атрыбутаў:

type Validators<T> = {
  [K in keyof T as T[K] extends Function ? never : `validate${Capitalize<string & K>}`]:
    (value: T[K]) => value is T[K];
};
interface ApiResponse {
  id: number;
  email: string;
  active: boolean;
  refresh: () => void;
}type ResponseValidators = Validators<ApiResponse>;
/*
{
  validateId: (value: number) => value is number;
  validateEmail: (value: string) => value is string;
  validateActive: (value: boolean) => value is boolean;
}
*/

Разбіранне: keyof задае ключы; цикл адпрацоўкі перавідае кожны з іх; as перайменоўвае або выкасывае элементы за дапамогою never, калі T[K] extends Function; extends вяршыць, чы рэшаць застаўіць або выкасыць; infer быў бы викорыстаны, якбы трэба было выявіць типы вёсканых аргументаў.

Короткая інструкцыя

Функцыя Ролі Дзе викорыстоўваецца
extends На рэвэлі типаў Порэйнаванне типаў
infer Толькі ўсередине extends
[K in keyof T] Ключы ціклу Тыпы, якія былі перапрацаваны as Перэіменаванне/выдалення ключоў Усередине клазу as для перапрацавання

Калі ўжо натыснуць на іх, складныя форматы больш не змушаюць викорыстоўваць any. Перазбудуйце Pick, Omit, Record і ReturnType з нуля — усі вышэйзгаданы функцыяны будуць видны — і інструментары на рэвэлі тыпа стане практычным, а не містычным. Автары бібліятэкаў паводзяцца за аднаковымі чатырма параметрамі; чытанне ўсіх іх файлаў .d.ts становіцца набліжна простым пасля таго, як за выходны перазбудуваць гэтыя інструменты вручную.

Шлях практыкавання

Паўторна створэнне элемента Exclude за дапамою умовных выразаў, ReturnType — за дапамою функцыі infer, элементаў Partial/Readonly — за дапамою мапаваных модифікатораў, а таксама спецыяльнага элемента OmitByType з параметрам as never. Гэтыя чатыры завданні выкорыстоўваюць усе функцыяны, практыкуемыя ў гэтым кіясе. Пасля чаго чытанне файлаў .d.ts бібліятэк перестае адчувацца як гэмпіранг коду і стае схожым на звычны TypeScript — толькі на рэвэле типаў.

Калі памылка з типам ператвараецца на масу умовных выразаў, трэба адразу знайсці рашэння: тымчасова заменіць мапаваны тип на конкрэтны аб’ектны тип або заменіць infer R на явную анотацыю, пакуль не стане зрозумелым той ветка коду, якая выклікае проблему. Дэбаггін на рэвэле типаў застаецца дэбаггінам; прыменяецца тая ж сама стратэгія роздзелення на часткі.

Лепшае выкарыстоўваць іменаваныя аліясы замест вялічзіных прыказаў, каб будучы чытальнікі моглі разумець намер. Экспортуйце тыя функцыі, якія ваша аплікацыя насправды павтарае выкарыстоўвае; спекулятивныя інструменты заставіце ў «сандбоксе» пакуль не з’явіцца другое месца вызову. Так спецыфікацыя типаў застанется такой жа намеранай, як і API часу выконання.