![](https://habrastorage.org/webt/um/fe/t_/umfet_kngorlggfmgokzowwtsuu.png)
Привет, друзья!
Представляю вашему вниманию перевод нескольких статей из серии Mastering TypeScript, посвященных углубленному изучению TypeScript.
- TypeScript в деталях. Полная версия
- TypeScript в деталях. Часть 2
- TypeScript в деталях. Часть 3
- Полезные возможности современного TypeScript
- Карманная книга по TypeScript
- Шпаргалка по TypeScript
T
, K
и V
в дженериках
![](https://habrastorage.org/webt/6j/yg/17/6jyg17pkl43smu0jxhwlim0njnu.gif)
T
называется параметром общего типа (generic type parameter). Это заменитель (placeholder) настоящего (actual) типа, передаваемого функции.
Суть такая: берем тип, определенный пользователем, и привязываем (chain) его к типу параметра функции и типу возвращаемого функцией значения.
![](https://habrastorage.org/webt/-z/ul/qw/-zulqwj_xrctzytk2_reaxwekos.gif)
Так что все-таки означает T
? T
означает тип (type). На самом деле, вместо T
можно использовать любое валидное название. Часто в сочетании с T
используются такие общие переменные, как K
, V
, E
и др.
K
представляет тип ключа объекта;V
представляет тип значения объекта;E
представляет тип элемента.
![](https://habrastorage.org/webt/uk/_r/dy/uk_rdymaxcj_-qfxjphhlaxstme.gif)
Разумеется, мы не ограничены одним параметром типа — их может быть сколько угодно:
![](https://habrastorage.org/webt/x0/0l/eh/x00lehgykaurlzzu3x1hbwtfn7s.gif)
При вызове функции identity
можно явно определить действительный тип параметра типа. Или можно позволить TypeScript
самостоятельно сделать вывод относительного него:
![](https://habrastorage.org/webt/m3/vy/5x/m3vy5xrj8_xd1alf6gqle02krco.gif)
Условные типы
Приходилось ли вам использовать утилиты типов Exclude
, Extract
, NonNullable
, Parameters
и ReturnType
?
Все эти утилиты основаны на условных типах (conditional types):
![](https://habrastorage.org/webt/re/9c/-l/re9c-l5l3ss1ictmvm8pkob3ma0.gif)
Здесь представлена лишь часть процесса
Краткая справка:
![](https://habrastorage.org/webt/ht/5m/mc/ht5mmcg-ax6n8t9nbollhsxztjo.jpeg)
Названные утилиты используются для следующих целей:
Exclude
— генерирует новый тип посредством исключения изUnionType
всех членов объединения, указанных вExcludedMembers
;Extract
— генерирует новый тип посредством извлечения изType
всех членов объединения, указанных вUnion
;NonNullable
— генерирует новый тип посредством исключенияnull
иundefined
изType
;Parameters
— генерирует новый кортеж (tuple) из типов параметров функцииType
;ReturnType
— генерирует новый тип, содержащий тип значения, возвращаемого функциейType
.
Примеры использования этих утилит:
![](https://habrastorage.org/webt/tq/8c/g_/tq8cg_0a-c2jojq3_vabbix76pg.jpeg)
Синтаксис условных типов:
T extends U ? X : Y
T
, U
, X
и Y
— заменители типов (см. выше). Сигнатуру можно понимать следующим образом: если T
может быть присвоен U
, возвращается тип X
, иначе возвращается тип Y
. Это чем-то напоминает тернарный оператор в JavaScript
.
![](https://habrastorage.org/webt/ht/jv/p3/htjvp3me0o8scjw2l2y59vtvpji.gif)
Как условные типы используются? Рассмотрим пример:
type IsString<T> = T extends string ? true : false;
type I0 = IsString<number>; // false
type I1 = IsString<"abc">; // true
type I2 = IsString<any>; // boolean
type I3 = IsString<never>; // never
![](https://habrastorage.org/webt/po/6u/wv/po6uwvfvnydqjrvyvtdmy19hxbs.gif)
Утилита IsString
позволяет определять, является ли действительный тип, переданный в качестве параметра типа, строковым типом. В дополнение к этому, с помощью условных типов и условных цепочек (conditional chain) можно определять несколько типов за один раз:
![](https://habrastorage.org/webt/9d/ve/pp/9dveppff0vo-nd4ymjyqguv7bio.jpeg)
Условная цепочка похожа на тернарные выражения в JS
:
![](https://habrastorage.org/webt/9k/kn/jj/9kknjjzqz07t5ybf-bg4vgdjb-g.jpeg)
Вопрос: что будет, если передать TypeName
объединение (union)?
// "string" | "function"
type T10 = TypeName<string | (() => void)>;
// "string" | "object" | "undefined"
type T11 = TypeName<string | string[] | undefined>;
![](https://habrastorage.org/webt/fs/9i/zu/fs9izufmtm4_b48d3mqzeg9syqg.gif)
Почему типы T10
и T11
возвращают объединения? Это объясняется тем, что TypeName
— это распределенный (distributed) условный тип. Условный тип называется распределенным, если проверяемый тип является "голым" (naked), т. е. не обернут в массив, кортеж, промис и т. д.
![](https://habrastorage.org/webt/1i/2b/gi/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)
![](https://habrastorage.org/webt/9j/ms/rs/9jmsrsx8c0nyi9gb45wemg7f9iu.gif)
Рассмотрим пример:
![](https://habrastorage.org/webt/hh/2i/vc/hh2ivccguqxfbgu9fgfgoelvpoi.jpeg)
Если параметр типа обернут в условный тип, он не будет распределенным, поэтому процесс не разбивается на отдельные ветки.
Рассмотрим поток выполнения (execution flow) встроенной утилиты Exclude
:
type Exclude<T, U> = 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"
![](https://habrastorage.org/webt/hs/qu/bp/hsqubpyvwbij-zz46cs1l_28ifu.gif)
Пример реализации утилиты с помощью условных и связанных (mapped, см. Заметка о Mapped Types и других полезных возможностях современного TypeScript) типов:
![](https://habrastorage.org/webt/dw/oy/7g/dwoy7gpqlg-qvs9a_dxnrxrzwac.jpeg)
![](https://habrastorage.org/webt/fv/xw/cs/fvxwcsrphguvdit9xlcuaredcec.jpeg)
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
interface User {
id: number;
name: string;
age: number;
updateName(newName: string): void;
}
type T5 = FunctionPropertyNames<User>; // "updateName"
type T6 = FunctionProperties<User>; // { updateName: (newName: string) => void; }
type T7 = NonFunctionPropertyNames<User>; // "id" | "name" | "age"
type T8 = NonFunctionProperties<User>; // { id: number; name: string; age: number; }
Данные утилиты позволяют легко извлекать атрибуты функциональных и нефункциональных типов, а также связанные с ними объектные типы из типа User
.
Оператор keyof
Приходилось ли вам использовать утилиты типов Partial
, Required
, Pick
и Record
?
![](https://habrastorage.org/webt/ze/_e/0y/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
:
![](https://habrastorage.org/webt/yc/_j/dq/yc_jdqjn-qsjh3trxavtlxa7fzo.jpeg)
В сообщениях об ошибках говорится о том, что obj
и key
имеют неявные типы any
. Для решения проблемы можно явно определить типы параметров:
![](https://habrastorage.org/webt/-9/dq/el/-9dqelqjxqn_gv0ordoqjnlalci.jpeg)
Получаем другую ошибку. Для правильного решения следует использовать параметр общего типа (generic) и keyof
:
function getProperty<T extends object, K extends keyof T>(
obj: T, key: K
) {
return obj[key];
}
Определяем 2 параметра типа: T
и K
. extends
применяется, во-первых, для ограничения (constraint) типа, передаваемого T
, подтипом объекта, во-вторых, для ограничения типа, передаваемого K
, подтипом объединения ключей объекта.
При отсутствии ключа TS
генерирует следующее сообщение об ошибке:
![](https://habrastorage.org/webt/y3/z7/td/y3z7tdkm8n62ytbvoh26irtrkqw.jpeg)
Оператор keyof
может применяться не только к объектам, но также к примитивам, типу any
, классам и перечислениям.
![](https://habrastorage.org/webt/0p/l1/ah/0pl1ahdtw8vube9crs45mfsnxgy.jpeg)
Рассмотрим поток выполнения (execution flow) утилиты Partial
:
![](https://habrastorage.org/webt/on/uj/vi/onujvikybbefyno8n8y6eolvcce.jpeg)
/**
* Делает все свойства T опциональными.
* typescript/lib/lib.es5.d.ts
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};
![](https://habrastorage.org/webt/1x/2g/mi/1x2gmisencmrmup8w_uwgwqvaui.gif)
Оператор typeof
Рассмотрим несколько полезных примеров использования оператора typeof
.
1. Получение типа объекта
![](https://habrastorage.org/webt/cr/n8/yr/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-объект
:
![](https://habrastorage.org/webt/tn/3x/m5/tn3xm5st8_j6jqbghbyx-qxvkee.jpeg)
Поэтому к перечислениям также можно применять оператор typeof
. Однако в случае с перечислениями, typeof
обычно комбинируется с оператором keyof
:
![](https://habrastorage.org/webt/p5/48/js/p548jsod1phiqkonhza8b2vmil4.jpeg)
3. Получение типа функции
Другим примером использования typeof
является получение типа функции (функция в JS
— это тоже объект). После получения типа функции можно воспользоваться встроенными утилитами типов ReturnType
и Parameters
для получения типа возвращаемого функцией значение и типа ее параметров:
![](https://habrastorage.org/webt/si/8s/mh/si8smh05eho5glzluncyiskd0qe.jpeg)
4. Получение типа класса
![](https://habrastorage.org/webt/fy/a7/qa/fya7qambyrsntleqxcnjcnx7gme.jpeg)
В приведенном примере createPoint
— это фабричная функция, создающая экземпляры класса Point
. С помощью typeof
можно получить сигнатуру конструктора класса Point
для реализации проверки соответствующего типа. При отсутствии typeof
в определении типа конструктора возникнет ошибка:
![](https://habrastorage.org/webt/9o/rj/-x/9orj-xdyf1oj0luk3leuie3fohm.jpeg)
6. Получение более точного типа
Использование typeof
в сочетании с утверждением const
(const assertion), представленным в TS 3.4
, позволяет получать более точные (precise) типы:
![](https://habrastorage.org/webt/nv/ai/w8/nvaiw8j0-dr_ap-yo-ao2pus9us.jpeg)
Надеюсь, что вы, как и я, нашли для себя что-то интересное. Благодарю за внимание и happy coding!