Привет, друзья!
Представляю вашему вниманию перевод нескольких статей из серии Mastering TypeScript, посвященных углубленному изучению TypeScript.
Предполагается, что вы имеете некоторый опыт работы с TS
. Если нет, вот Карманная книга по TS.
T
, K
и V
в дженериках
T
называется параметром общего типа (generic type parameter). Это заменитель (placeholder) настоящего (actual) типа, передаваемого функции.
Суть такая: берем тип, определенный пользователем, и привязываем (chain) его к типу параметра функции и типу возвращаемого функцией значения.
Так что все-таки означает T
? T
означает тип (type). На самом деле, вместо T
можно использовать любое валидное название. Часто в сочетании с T
используются такие общие переменные, как K
, V
, E
и др.
-
K
представляет тип ключа объекта; -
V
представляет тип значения объекта; -
E
представляет тип элемента.
Разумеется, мы не ограничены одним параметром типа — их может быть сколько угодно:
При вызове функции identity
можно явно определить действительный тип параметра типа. Или можно позволить TypeScript
самостоятельно сделать вывод относительного него:
Условные типы
Приходилось ли вам использовать утилиты типов Exclude
, Extract
, NonNullable
, Parameters
и ReturnType
?
Все эти утилиты основаны на условных типах (conditional types):
Здесь представлена лишь часть процесса
Краткая справка:
Названные утилиты используются для следующих целей:
-
Exclude
— генерирует новый тип посредством исключения изUnionType
всех членов объединения, указанных вExcludedMembers
; -
Extract
— генерирует новый тип посредством извлечения изType
всех членов объединения, указанных вUnion
; -
NonNullable
— генерирует новый тип посредством исключенияnull
иundefined
изType
; -
Parameters
— генерирует новый кортеж (tuple) из типов параметров функцииType
; -
ReturnType
— генерирует новый тип, содержащий тип значения, возвращаемого функциейType
.
Примеры использования этих утилит:
Синтаксис условных типов:
T extends U ? X : Y
T
, U
, X
и Y
— заменители типов (см. выше). Сигнатуру можно понимать следующим образом: если T
может быть присвоен U
, возвращается тип X
, иначе возвращается тип Y
. Это чем-то напоминает тернарный оператор в JavaScript
.
Как условные типы используются? Рассмотрим пример:
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
Утилита IsString
позволяет определять, является ли действительный тип, переданный в качестве параметра типа, строковым типом. В дополнение к этому, с помощью условных типов и условных цепочек (conditional chain) можно определять несколько типов за один раз:
Условная цепочка похожа на тернарные выражения в JS
:
Вопрос: что будет, если передать TypeName
объединение (union)?
// "string" | "function"
type T10 = TypeName<string | (() => void)>;
// "string" | "object" | "undefined"
type T11 = TypeName<string | string[] | undefined>;
Почему типы T10
и T11
возвращают объединения? Это объясняется тем, что TypeName
— это распределенный (distributed) условный тип. Условный тип называется распределенным, если проверяемый тип является "голым" (naked), т. е. не обернут в массив, кортеж, промис и т. д.
В случае с распределенными условными типами, когда проверяемый тип является объединением, оно разбивается на несколько веток в процессе выполнения операции:
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)
Рассмотрим пример:
Если параметр типа обернут в условный тип, он не будет распределенным, поэтому процесс не разбивается на отдельные ветки.
Рассмотрим поток выполнения (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"
Пример реализации утилиты с помощью условных и связанных (mapped, см. Заметка о Mapped Types и других полезных возможностях современного TypeScript) типов:
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
?
Внутри всех этих утилит используется оператор 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
:
В сообщениях об ошибках говорится о том, что obj
и key
имеют неявные типы any
. Для решения проблемы можно явно определить типы параметров:
Получаем другую ошибку. Для правильного решения следует использовать параметр общего типа (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
генерирует следующее сообщение об ошибке:
Оператор keyof
может применяться не только к объектам, но также к примитивам, типу any
, классам и перечислениям.
Рассмотрим поток выполнения (execution flow) утилиты Partial
:
/**
* Делает все свойства T опциональными.
* typescript/lib/lib.es5.d.ts
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};
Оператор typeof
Рассмотрим несколько полезных примеров использования оператора typeof
.
1. Получение типа объекта
Объект 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-объект
:
Поэтому к перечислениям также можно применять оператор typeof
. Однако в случае с перечислениями, typeof
обычно комбинируется с оператором keyof
:
3. Получение типа функции
Другим примером использования typeof
является получение типа функции (функция в JS
— это тоже объект). После получения типа функции можно воспользоваться встроенными утилитами типов ReturnType
и Parameters
для получения типа возвращаемого функцией значение и типа ее параметров:
4. Получение типа класса
В приведенном примере createPoint
— это фабричная функция, создающая экземпляры класса Point
. С помощью typeof
можно получить сигнатуру конструктора класса Point
для реализации проверки соответствующего типа. При отсутствии typeof
в определении типа конструктора возникнет ошибка:
6. Получение более точного типа
Использование typeof
в сочетании с утверждением const
(const assertion), представленным в TS 3.4
, позволяет получать более точные (precise) типы:
Надеюсь, что вы, как и я, нашли для себя что-то интересное. Благодарю за внимание и happy coding!
Комментарии (4)
Stonuml
02.09.2022 13:46Для себя открыл typeof. Особенно актуально мне кажется для описания объекта state, если у него есть дефолтное состояние
eshimischi
02.09.2022 23:49+1Спасибо за перевод данного автора, прошелся по оригинальным статьям, как раз то что нужно для начавшего изучать TS. Удачи с переводами!
Aleks_ja
Очень интересно! Но ничего не понял.
Metotron0
Немножко изучаю TS, и похоже, что эта статья для тех, кто что-то знает, но не всё понимает, а хочет лучше понять.