[Перевод] TypeScript в деталях. Часть 1

umfet_kngorlggfmgokzowwtsuu.png


Привет, друзья!

Представляю вашему вниманию перевод нескольких статей из серии Mastering TypeScript, посвященных углубленному изучению TypeScript.


T, K и V в дженериках


6jyg17pkl43smu0jxhwlim0njnu.gif

T называется параметром общего типа (generic type parameter). Это заменитель (placeholder) настоящего (actual) типа, передаваемого функции.

Суть такая: берем тип, определенный пользователем, и привязываем (chain) его к типу параметра функции и типу возвращаемого функцией значения.


-zulqwj_xrctzytk2_reaxwekos.gif

Так что все-таки означает T? T означает тип (type). На самом деле, вместо T можно использовать любое валидное название. Часто в сочетании с T используются такие общие переменные, как K, V, E и др.


  • K представляет тип ключа объекта;
  • V представляет тип значения объекта;
  • E представляет тип элемента.


uk_rdymaxcj_-qfxjphhlaxstme.gif

Разумеется, мы не ограничены одним параметром типа — их может быть сколько угодно:


x00lehgykaurlzzu3x1hbwtfn7s.gif

При вызове функции identity можно явно определить действительный тип параметра типа. Или можно позволить TypeScript самостоятельно сделать вывод относительного него:


m3vy5xrj8_xd1alf6gqle02krco.gif

Условные типы

Приходилось ли вам использовать утилиты типов Exclude, Extract, NonNullable, Parameters и ReturnType?

Все эти утилиты основаны на условных типах (conditional types):


re9c-l5l3ss1ictmvm8pkob3ma0.gif

Здесь представлена лишь часть процесса

Краткая справка:


ht5mmcg-ax6n8t9nbollhsxztjo.jpeg

Названные утилиты используются для следующих целей:


  • Exclude — генерирует новый тип посредством исключения из UnionType всех членов объединения, указанных в ExcludedMembers;
  • Extract — генерирует новый тип посредством извлечения из Type всех членов объединения, указанных в Union;
  • NonNullable — генерирует новый тип посредством исключения null и undefined из Type;
  • Parameters — генерирует новый кортеж (tuple) из типов параметров функции Type;
  • ReturnType — генерирует новый тип, содержащий тип значения, возвращаемого функцией Type.

Примеры использования этих утилит:


tq8cg_0a-c2jojq3_vabbix76pg.jpeg

Синтаксис условных типов:

T extends U ? X : Y

T, U, X и Y — заменители типов (см. выше). Сигнатуру можно понимать следующим образом: если T может быть присвоен U, возвращается тип X, иначе возвращается тип Y. Это чем-то напоминает тернарный оператор в JavaScript.


htjvp3me0o8scjw2l2y59vtvpji.gif

Как условные типы используются? Рассмотрим пример:

type IsString = T extends string ? true : false;
​
type I0 = IsString;  // false
type I1 = IsString<"abc">;  // true
type I2 = IsString;  // boolean
type I3 = IsString;  // never


po6uwvfvnydqjrvyvtdmy19hxbs.gif

Утилита IsString позволяет определять, является ли действительный тип, переданный в качестве параметра типа, строковым типом. В дополнение к этому, с помощью условных типов и условных цепочек (conditional chain) можно определять несколько типов за один раз:


9dveppff0vo-nd4ymjyqguv7bio.jpeg

Условная цепочка похожа на тернарные выражения в JS:


9kknjjzqz07t5ybf-bg4vgdjb-g.jpeg

Вопрос: что будет, если передать TypeName объединение (union)?

// "string" | "function"
type T10 = TypeName void)>;
// "string" | "object" | "undefined"
type T11 = TypeName;


fs9izufmtm4_b48d3mqzeg9syqg.gif

Почему типы T10 и T11 возвращают объединения? Это объясняется тем, что TypeName — это распределенный (distributed) условный тип. Условный тип называется распределенным, если проверяемый тип является «голым» (naked), т. е. не обернут в массив, кортеж, промис и т. д.


1i2bgiobeeop-l2ntqacurmdqkw.jpeg

В случае с распределенными условными типами, когда проверяемый тип является объединением, оно разбивается на несколько веток в процессе выполнения операции:

T extends U ? X : Y
T => A | B | C
A | B | C extends U ? X : Y  =>
(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)


9jmsrsx8c0nyi9gb45wemg7f9iu.gif

Рассмотрим пример:


hh2ivccguqxfbgu9fgfgoelvpoi.jpeg

Если параметр типа обернут в условный тип, он не будет распределенным, поэтому процесс не разбивается на отдельные ветки.

Рассмотрим поток выполнения (execution flow) встроенной утилиты Exclude:

type Exclude = T extends U ? never : T;
type T4 = Exclude<"a" | "b" | "c", "a" | "b">
​
("a" extends "a" | "b" ? never : "a") // => never
| ("b" extends "a" | "b" ? never : "b") // => never
| ("c" extends "a" | "b" ? never : "c") // => "c"
​
never | never | "c" // => "c"


hsqubpyvwbij-zz46cs1l_28ifu.gif

Пример реализации утилиты с помощью условных и связанных (mapped, см. Заметка о Mapped Types и других полезных возможностях современного TypeScript) типов:


dwoy7gpqlg-qvs9a_dxnrxrzwac.jpeg
fvxwcsrphguvdit9xlcuaredcec.jpeg

type FunctionPropertyNames = {
    [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type FunctionProperties = Pick>;
​
type NonFunctionPropertyNames = {
    [K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
type NonFunctionProperties = Pick>;
​
interface User {
  id: number;
  name: string;
  age: number;
  updateName(newName: string): void;
}
​
type T5 = FunctionPropertyNames; // "updateName"
type T6 = FunctionProperties; // { updateName: (newName: string) => void; }
type T7 = NonFunctionPropertyNames; // "id" | "name" | "age"
type T8 = NonFunctionProperties; // { id: number; name: string; age: number; }

Данные утилиты позволяют легко извлекать атрибуты функциональных и нефункциональных типов, а также связанные с ними объектные типы из типа User.


Оператор keyof

Приходилось ли вам использовать утилиты типов Partial, Required, Pick и Record?


ze_e0yjqw_uvhfkdedsdbiwnuk4.jpeg

Внутри всех этих утилит используется оператор keyof.

В JS ключи объекта извлекаются с помощью метода Object.keys:

const user = {
  id: 666,
  name: "bytefer",
}
const keys = Object.keys(user); // ["id", "name"]

В TS это делается с помощью keyof:

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

После получения ключа объектного типа, мы можем получить доступ к типу значения, соответствующему данному ключу, с помощью синтаксиса, аналогичного синтаксису доступа к свойству объекта:

type U1 = User["id"] // number
type U2 = User["id" | "name"] // string | number
type U3 = User[keyof User] // string | number

В приведенном примере используется тип индексированного доступа (indexed access type) для получения типа определенного свойства типа User.

Как keyof используется на практике? Рассмотрим пример:

function getProperty(obj, key) {
  return obj[key];
}
const user = {
  id: 666,
  name: "bytefer",
}
const userName = getProperty(user, "name");

Функция getProperty принимает 2 параметра: объект (obj) и ключ (key), и возвращает значение объекта по ключу.

Перенесем данную функцию в TS:


yc_jdqjn-qsjh3trxavtlxa7fzo.jpeg

В сообщениях об ошибках говорится о том, что obj и key имеют неявные типы any. Для решения проблемы можно явно определить типы параметров:


-9dqelqjxqn_gv0ordoqjnlalci.jpeg

Получаем другую ошибку. Для правильного решения следует использовать параметр общего типа (generic) и keyof:

function getProperty(
  obj: T, key: K
) {
  return obj[key];
}

Определяем 2 параметра типа: T и K. extends применяется, во-первых, для ограничения (constraint) типа, передаваемого T, подтипом объекта, во-вторых, для ограничения типа, передаваемого K, подтипом объединения ключей объекта.

При отсутствии ключа TS генерирует следующее сообщение об ошибке:


y3z7tdkm8n62ytbvoh26irtrkqw.jpeg

Оператор keyof может применяться не только к объектам, но также к примитивам, типу any, классам и перечислениям.


0pl1ahdtw8vube9crs45mfsnxgy.jpeg

Рассмотрим поток выполнения (execution flow) утилиты Partial:


onujvikybbefyno8n8y6eolvcce.jpeg

/**
 * Делает все свойства T опциональными.
 * typescript/lib/lib.es5.d.ts
 */
type Partial = {
    [P in keyof T]?: T[P];
};


1x2gmisencmrmup8w_uwgwqvaui.gif

Оператор typeof

Рассмотрим несколько полезных примеров использования оператора typeof.

1. Получение типа объекта


crn8yrrts2i7ywm0y2p8n3qkfmi.jpeg

Объект man — это обычный объект JS. Для определения его типа в TS можно использовать type или interface. Тип объекта позволяет применять встроенные утилиты типов, такие как Partial, Required, Pick или Readonly, для генерации производных типов.

Для небольших объектов ручное определение типа не составляет труда, но для больших и сложных объектов с несколькими уровнями вложенности это может быть утомительным. Вместо ручного определения типа объекта можно прибегнуть к помощи оператора typeof:

type Person = typeof man;

type Address = Person["address"];

Person["address"] — это тип индексированного доступа (indexed access type), позволяющий извлекать тип определенного свойства (address) из другого типа (Person).

2. Получение типа, представляющего все ключи перечисления в виде строк

В TS перечисление (enum) — это специальный тип, компилирующийся в обычный JS-объект:


tn3xm5st8_j6jqbghbyx-qxvkee.jpeg

Поэтому к перечислениям также можно применять оператор typeof. Однако в случае с перечислениями, typeof обычно комбинируется с оператором keyof:


p548jsod1phiqkonhza8b2vmil4.jpeg

3. Получение типа функции

Другим примером использования typeof является получение типа функции (функция в JS — это тоже объект). После получения типа функции можно воспользоваться встроенными утилитами типов ReturnType и Parameters для получения типа возвращаемого функцией значение и типа ее параметров:


si8smh05eho5glzluncyiskd0qe.jpeg

4. Получение типа класса


fya7qambyrsntleqxcnjcnx7gme.jpeg

В приведенном примере createPoint — это фабричная функция, создающая экземпляры класса Point. С помощью typeof можно получить сигнатуру конструктора класса Point для реализации проверки соответствующего типа. При отсутствии typeof в определении типа конструктора возникнет ошибка:


9orj-xdyf1oj0luk3leuie3fohm.jpeg

6. Получение более точного типа

Использование typeof в сочетании с утверждением const (const assertion), представленным в TS 3.4, позволяет получать более точные (precise) типы:


nvaiw8j0-dr_ap-yo-ao2pus9us.jpeg

Надеюсь, что вы, как и я, нашли для себя что-то интересное. Благодарю за внимание и happy coding!


p-u9l27ynelxi92bcmdxhu76ma8.png

© Habrahabr.ru