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


Представляю вашему вниманию перевод нескольких статей из серии 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)


  1. Aleks_ja
    02.09.2022 12:05
    +4

    Очень интересно! Но ничего не понял.


    1. Metotron0
      02.09.2022 13:15
      +3

      Немножко изучаю TS, и похоже, что эта статья для тех, кто что-то знает, но не всё понимает, а хочет лучше понять.


  1. Stonuml
    02.09.2022 13:46

    Для себя открыл typeof. Особенно актуально мне кажется для описания объекта state, если у него есть дефолтное состояние


  1. eshimischi
    02.09.2022 23:49
    +1

    Спасибо за перевод данного автора, прошелся по оригинальным статьям, как раз то что нужно для начавшего изучать TS. Удачи с переводами!