TypeScript. Начало работы, часть 7 из 7
15.04.2023
Теги: JavaScript • TypeScript • Web-разработка • Теория • ТипыДанных
Продвинутый TypeScript
1. Оператор keyof
В JavaScript ключи объекта извлекаются с помощью метода Object.keys
const user = { id: 12345, name: 'Сергей', }; const keys = Object.keys(user); // ['id', 'name']
В TypeScript это делается с помощью оператора keyof
interface 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: object, key: string) {
return obj[key];
}
const user = {
id: 12345,
name: 'Сергей',
};
const userName = getProperty(user, 'name');
TypeScript выдает ошибку на единственной строке тела функции.
Элемент неявно содержит тип «any», так как выражение типа «string» не может быть использовано для индексации типа «{}». Element implicitly has an «any» type because expression of type «string» can't be used to index type «{}».
Эта ошибка означает, что TypeScript не знает, может ли параметр key
типа string
быть индексом параметра obj
типа object
. Чтобы избавиться от ошибки, нам надо сообщить TypeScript тип ключей, которые может использовать параметр obj
.
interface BaseObject {
[key: string]: any,
}
function getProperty(obj: BaseObject, key: string) {
return obj[key];
}
const user = {
id: 12345,
name: 'Сергей',
};
const userName = getProperty(user, 'name');
От ошибки мы избавились, но сейчас мы можем вызвать функцию getProperty
, передавая в качестве второго аргумента несуществующее свойство age
. И об этой ошибке TypeScript
нам не сообщит, пока мы не столкнемся с последствиями уже на этапе выполнения.
const userName = getProperty(user, 'age');
Мы можем исправить это, если используем обобщённый тип (generic type)
function getProperty<T extends object, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const user = {
id: 12345,
name: 'Сергей',
};
const userName = getProperty(user, 'age'); // ошибка
Мы определяем два параметра типа — T
и K
. Ключевое слово extends
применяется, во-первых, для ограничения (constraint) типа, передаваемого T
, подтипом объекта, во-вторых, для ограничения типа, передаваемого K
, подтипом объединения ключей объекта.
Оператор keyof
может применяться не только к объектам, но также к примитивам, типу any
, классам и перечислениям.
class Person {
id = 12345;
name = 'Сергей';
}
type P = keyof Person; // 'id' | 'name'
enum HttpMethod {
Get,
Post,
}
type M = keyof typeof HttpMethod; // 'Get' | 'Post'
2. Оператор typeof
Рассмотрим несколько полезных примеров использования оператора typeof
.
2.1. Получение типа объекта
Для небольших объектов ручное определение типа не составляет труда, но для больших и сложных объектов с несколькими уровнями вложенности это может быть утомительным.
const country = {
name: 'Германия',
language: 'немецкий',
capital: {
name: 'Берлин',
population: 3375000,
year: 1237,
},
};
interface Country {
name: string;
language: string;
capital: {
name: string;
population: number;
year: number;
};
}
Вместо ручного определения типа объекта можно прибегнуть к помощи оператора typeof
.
const country = {
name: 'Германия',
language: 'немецкий',
capital: {
name: 'Берлин',
population: 3375000,
year: 1237,
},
};
type Country = typeof country;
type Capital = Country['capital'];
2.2. Получение типа, представляющего все ключи перечисления в виде строк
В TypeScript перечисление (enum) — это специальный тип, компилирующийся в обычный javascript-объект.
enum HttpMethod {
GET,
POST,
PUT,
DELETE,
}
"use strict"; var HttpMethod; (function (HttpMethod) { HttpMethod[HttpMethod["GET"] = 0] = "GET"; HttpMethod[HttpMethod["POST"] = 1] = "POST"; HttpMethod[HttpMethod["PUT"] = 2] = "PUT"; HttpMethod[HttpMethod["DELETE"] = 3] = "DELETE"; })(HttpMethod || (HttpMethod = {}));
Поэтому к перечислениям также можно применять оператор typeof
. Однако, в случае с перечислениями, typeof
комбинируется с оператором keyof
.
type Method = keyof typeof HttpMethod; // 'GET' | 'POST' | 'PUT' | 'DELETE'
2.3. Получение типа функции
Функция в JavaScript — это тоже объект, так что можно использовать typeof
для получения типа функции. После этого можно воспользоваться утилитами типов ReturnType
и Parameters
для получения типа возвращаемого функцией значение и типа ее параметров.
function sum(a: number, b: number) {
return a + b;
}
type SumType = typeof sum; // (a: number, b: number) => number
type SumReturnType = ReturnType<SumType>; // number
type SumParamsType = Parameters<SumType>; // [a: number, b: number]
2.4. Получение сигнатуры конструктора класса
В приведенном ниже примере createPoint
— это фабричная функция, создающая экземпляры класса Point
. С помощью typeof
можно получить сигнатуру конструктора класса Point
для реализации проверки соответствующего типа.
class Point {
constructor(public x: number, public y: number) {}
}
interface PointConstructorType {
new (x: number, y: number): Point;
}
function createPoint(
PointConstructor: PointConstructorType,
x: number,
y: number
) {
return new PointConstructor(x, y);
}
class Point {
constructor(public x: number, public y: number) {}
}
function createPoint(
// typeof позволяет получить сигнатуру конструктора
PointConstructor: typeof Point,
x: number,
y: number
) {
return new PointConstructor(x, y);
}
3. Связанные типы
Допустим, у нас есть тип User
, в котором все поля являются обязательными.
type User = {
name: string;
password: string;
address: string;
phone: string;
}
Потом потребовалось создать тип OptionalUser
, в котором все поля являются необязательными.
type OptionalUser = {
name?: string;
password?: string;
address?: string;
phone?: string;
}
Потом еще потребовалось создать тип ReadonlyUser
, в котором все поля только для чтения.
type ReadonlyUser = {
readonly name: string;
readonly password: string;
readonly address: string;
readonly phone: string;
}
В итоге получилось много дублирующегося кода, который хотелось бы сократить для удобства работы.
И здесь нам на помощь приходят связанные типы, позволяющими связывать тип исходного объекта с типом нового объекта.
Синтаксис связанных типов простой. P in K
можно сравнить с инструкцией for..in
, она используется для перебора всех ключей типа K
. Тип T
— это любой тип, валидный с точки зрения TypeScript.
В процессе связывания типов могут использоваться дополнительные модификаторы «опциональный» и «только для чтения». Соответствующие модификаторы добавляются и удаляются с помощью символов плюс и минус. По умолчанию модификатор добавляется, то есть используется плюс.
{ [ P in K ] : T }
{ [ P in K ] ?: T }
{ [ P in K ] -?: T }
{ readonly [ P in K ] : T }
{ readonly [ P in K ] ?: T }
{ -readonly [ P in K ] ?: T }
Теперь можем упростить наш код — для этого добавим типы OptionalProperties
и ReadonlyProperties
.
type User = {
name: string;
password: string;
address: string;
phone: string;
}
type OptionalProperties<T> = {
[P in keyof T]?: T[P];
};
type ReadonlyProperties<T> = {
readonly [P in keyof T]: T[P];
};
type OptionalUser = OptionalProperties<User>;
type ReadonlyUser = ReadonlyProperties<User>;
4. Условные типы
Условные типы (conditional types) позволяют вводить логику if/else
в определения типов и делать их более динамичными. Для этого используется тернарный синтаксис для определения условных операторов. Они начинаются с указания самого условия с применением нотации SomeType extends OtherType
, за которой следуют ветки этого условия true
и false
.
Вот очень простой условный тип, который будет создавать литеральные типы true
и false
в зависимости от получаемого параметра типа.
type IsOne<Type> = Type extends 1 ? true : false;
type TypeOne = IsOne<1>; // => true
type TypeTwo = IsOne<2>; // => false
Можно сказать, что условные типы (conditional types) дополняют возможности обобщенных типов (generic type). Рассмотрим еще один пример — реализация интерфейса Item
может работать со строкой и числом.
interface StringContainer {
value: string;
format(): string;
split(): string[];
}
interface NumberContainer {
value: number;
square(): number;
round(): number;
}
interface Item<Type> {
id: number;
container: Type extends string ? StringContainer : NumberContainer;
}
Можно также вложить несколько тернарных операторов для описания более сложных условий.
type ArrayItemType<Type> = Type extends number[]
? number
: Type extends string[]
? string
: Type extends boolean[]
? boolean
: unknown;
type ArrayItemNumber = ArrayItemType<number[]>; // => number
type ArrayIteString = ArrayItemType<string[]>; // => string
type ArrayItemBoolean = ArrayItemType<boolean[]>; // => boolean
Запись «Type extends string
» означает, что Тype
— это некий тип, являющийся подмножеством типа string
. Но так это работает только с примитивными типами — если мы используем вместо string
тип объекта с определенным набором свойств, то это наоборот означает, что Тype
является надмножеством этого типа.
type TypeOne = { a: string; b: number } extends { a: string } ? true : false; // true
type TypeTwo = { a: string } extends { a: string; b: number } ? true : false; // false
let one: { a: string } = {
a: 'some string',
};
let two: { a: string; b: number } = {
a: 'other string',
b: 12345,
};
one = two; // можно
two = one; // нельзя
5. Ключевое слово infer
Ключевое слово infer
расширяет возможности условных типов. Это что-то вроде ключевого слова var
, но для типов — позволяет получить и сохранить тип в переменной, чтобы потом вернуть. Для примера получим тип возвращаемого функцией значения.
type MyReturnType<T> = T extends (...args: any) => infer R ? R : never;
type ReturnTypeString = MyReturnType<() => string>; // string
type ReturnTypeNumber = MyReturnType<() => number>; // number
type ReturnTypeStringNumber = MyReturnType<() => string | number>; // string | number
type ReturnTypeArrayBoolean = MyReturnType<(a: boolean, b: boolean) => boolean[]>; // boolean[]
Когда T
принимает значение типа функции — возвращаемый функцией тип значения запоминается в R
и возвращается. В следующем примере получим тип параметров функции.
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type ParamsTypeString = MyParameters<(a: string) => void>; // [a: string]
type ParamsTypeNumber = MyParameters<(a: number) => void>; // [a: number]
type ParamsTypeStringNumber = MyParameters<(a: number, b: string) => void>; // [a: number, b: string]
type ParamsTypeArrayBoolean = MyParameters<(a: boolean[]) => void>; // [a: boolean[]]
6. Ключевое слово declare
Для установки связи с внешними файлами js-скриптов в TypeScript служат декларативные или заголовочные файлы. Это файлы с расширением .d.ts
, они описывают синтаксис и структуру функций и свойств, которые могут использоваться в приложении, не предоставляя при этом конкретной реализации.
Рассмотрим, как мы можем использовать заголовочные файлы. Иногда в javascript используются глобальные переменные, которые должны быть видны везде. Например, пусть на веб-странице (или в подключаемом файле javascript) в коде определена переменная.
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Заголовочные файлы в TypeScript</title> <script> let helloMessage = 'Hello TypeScript!'; </script> <script src="app.js"></script> </head> <body> <h1>Заголовочные файлы в TypeScript</h1> </body> </html>
И мы хотим использовать эту переменную в typescript-коде в файле app.ts
.
console.log(helloMessage);
При запуске приложения компилятор не сможет скомпилировать приложение, так как для TypeSscript глобальная переменная пока не существует. В этом случае нам надо подключить определение глобальной переменной с помощью декларативного файла. Для этого добавим в проект новый файл global.d.ts
, который будет иметь следующее содержимое.
declare let helloMessage: string;
Подобным образом мы можем подключать другие компоненты кода JavaScript — функции, объекты, классы. Допустим, на веб-странице в javascript-коде объявлены переменная, функция, объект и класс.
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Заголовочные файлы в TypeScript</title> <script> let helloMessage = 'Hello TypeScript!'; let printMessage = (message) => console.log(message); let user = { name: 'Сергей', age: 37, print() { console.log(`Имя ${this.name}, возраст ${this.age}`); } } class Person { constructor(name, age) { this.name = name; this.age = age; } display() { console.log(`Имя ${this.name}, возраст ${this.age}`); } } </script> <script src="app.js"></script> </head> <body> <h1>Заголовочные файлы в TypeScript</h1> </body> </html>
Для использования этой переменной, функции, объекта и класса в typescript-коде — файл global.d.ts
должен иметь следующий вид.
declare let helloMessage: string;
declare let printMessage: (message: string) => void;
declare const user: { name: string; age: number; print: () => void };
declare class Person {
name: string;
age: number;
constructor(name: string, age: number);
display(): void;
}
Заголовочные файлы для библиотек
При разработке приложения, как правило, приходится взаимодействовать с крупными библиотеками — например, jQuery. Чтобы использовать функционал этих библиотек в коде на TypeScript — для них надо создать свои файлы определений. И это может быть довольно утомительно в виду сложности библиотек и больших объемов кода. В сообществе TypeScript возникла идея создать общий репозиторий для подобных файлов — это DefinitelyTyped.
> npm install --save-dev @types/jquery
В проекте будет создана директория node_modules/@types
, в котором поддиректория jquery
будет хранить заголовочные файлы для библиотеки jQuery. Значение по умолчанию опции typeRoots
файла кофигурации tsconfig.json
предписывает искать файлы определений в директориях ./node_modules/@types/
, ../node_modules/@types/
, ../../node_modules/@types/
— и так далее. Если установить свое значение typeRoots
— будут включены только указанные пакеты, значение по умолчанию будет утеряно.
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Заголовочные файлы в TypeScript</title> <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script> <script src="app.js"></script> </head> <body> <div id="content"></div> </body> </html>
// файл app.ts
jQuery(function ($) {
$('#content').html('<h1>Заголовочные файлы в TypeScript</h1>');
});
7. Утилиты типа
Утилиты типа (utility types) позволяют легко конвертировать, извлекать, исключать типы, получать параметры типов и типы значений, возвращаемых функциями.
7.1. Partial<Type>
Данная утилита делает все свойства Type
опциональными (необязательными).
/**
* Make all properties in T optional.
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};
type User = {
name: string;
email: string;
phone: string;
};
type PartialUser = { // плохо
name?: string;
email?: string;
phone?: string;
};
type PartialUser = Partial<User>; // хорошо
7.2. Required<Type>
Данная утилита делает все свойства Type
обязательными (противоположность Partial
).
/**
* Make all properties in T required.
*/
type Required<Type> = {
[P in keyof T]-?: T[P];
};
type User = {
name?: string;
email?: string;
phone?: string;
};
type RequiredUser = { // плохо
name: string;
email: string;
phone: string;
};
type RequiredUser = Required<User>; // хорошо
7.3. Readonly<Type>
Данная утилита делает все свойства Type
доступными только для чтения (readonly
). Такие свойства являются иммутабельными (их значения нельзя изменять).
/**
* Make all properties in T readonly.
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type User = {
name: string;
email: string;
phone: string;
};
type ReadonlyUser = { // плохо
readonly name: string;
readonly email: string;
readonly phone: string;
};
type ReadonlyUser = Readonly<User>; // хорошо
7.4. Record<Type>
Данная утилита создает новый объектный тип (object type), ключами которого являются Keys
, а значениями свойств — Type
.
/**
* Construct a type with a set of properties K of type T.
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};
type Keys = 'a' | 'b' | 'c';
type RecordType = Record<Keys, number>;
// record может быть { a: number, b: number, c: number }
let record: RecordType;
interface CatInfo {
name: string;
age: number;
breed: string;
}
type CatIds =
| 'e690a91e-9269-4c1b-a112-342bef7f4be9'
| '78f2ffdb-8423-4f7c-9094-ef8838f43c4f'
| 'ffd041a6-42e9-4e71-92a3-7a7a5d495c43';
const cats: Record<CatIds, CatInfo> = {
'e690a91e-9269-4c1b-a112-342bef7f4be9': {
name: 'Васька',
age: 10,
breed: 'Персидская',
},
'78f2ffdb-8423-4f7c-9094-ef8838f43c4f': {
name: 'Мурка',
age: 5,
breed: 'Сиамская',
},
'ffd041a6-42e9-4e71-92a3-7a7a5d495c43': {
name: 'Барсик',
age: 12,
breed: 'Ангорская',
},
};
7.5. Exclude<UnionType, ExcludedMembers>
Данная утилита создает новый тип посредством исключения из UnionType
всех членов объединения, которые могут быть присвоены (assignable) ExcludedMembers
.
/**
* Exclude from T those types that are assignable to U.
*/
type Exclude<T, U> = T extends U ? never : T;
Простыми словами, из типа T
будут исключены признаки (ключи), присущие также и типу U
. Очень условно можно сказать, что из T
вычитается U
.
// из типа T=number|string исключается number, потому что number есть в U=number|boolean
// из типа T=number|string не исключается string, потому что string нет в U=number|boolean
type TypeOne = Exclude<number | string, number | boolean>; // string
// из типа T=number|string не исключается number, потому что number нет в U=boolean|object
// из типа T=number|string не исключается string, потому что string нет в U=boolean|object
type TypeTwo = Exclude<number | string, boolean | object>; // number|string
В случае, если оба аргумента типа принадлежат к одному и тому же типу данных, Exclude<T, U>
будет представлен типом never
.
7.6. Extract<Type, Union>
Данная утилита создает новый тип посредством извлечения из Type
всех членов объединения, которые могут быть присвоены Union
.
/**
* Extract from T those types that are assignable to U.
*/
type Extract<T, U> = T extends U ? T : never;
Простыми словами, из типа T
будут извлечены признаки (ключи), присущие также и типу U
. Новый тип будет содержать признаки (ключи), присущие обоим типам T
и U
. Очень условно можно сказать, что это пересечение T
и U
.
// из типа T=number|string извлекается number, потому number есть в U=number|string
// из типа T=number|string извлекается string, потому string есть в U=number|string
type TypeOne = Extract<number | string, number | string>; // number|string
// из типа T=number|string извлекается number, потому number есть в U=number|boolean
// из типа T=number|string не извлекается string, потому string нет в U=number|boolean
type TypeTwo = Extract<number | string, number | boolean>; // number
7.7. Pick<Type, Keys>
Данная утилита создает новый тип посредством извлечения из Type
набора (множества) свойств Keys
(Keys
— строковый литерал или их объединение).
/**
* From T, pick a set of properties whose keys are in the union K.
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type User = {
name: string;
email: string;
phone: string;
age: number;
};
type PartialUser = { // плохо
name: string;
phone: string;
};
type PartialUser = Pick<User, 'name' | 'phone'>; // хорошо
7.8. Omit<Type, Keys>
Данная утилита создает новый тип посредством исключения из Type
набора (множества) свойств Keys
(Keys
— строковый литерал или их объединение). Утилита Omit
является противоположностью утилиты Pick
.
/**
* Construct a type with the properties of T except for those in type K.
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type User = {
name: string;
email: string;
phone: string;
age: number;
};
type PartialUser = { // плохо
name: string;
phone: string;
};
type PartialUser = Omit<User, 'email' | 'age'>; // хорошо
7.9. NonNullable<Type>
Данная утилита создает новый тип посредством исключения из Type
типов null
и undefined
.
/**
* Exclude null and undefined from T.
*/
type NonNullable<T> = T extends null | undefined ? never : T;
type Value = string | number | null | undefined;
type NonNullableValue = string | number; // плохо
type NonNullableValue = NonNullable<Value>; // хорошо
7.10. Parameters<Type>
Данная утилита создает кортеж (tuple) из типов параметров функции Type
.
/**
* Obtain the parameters of a function type in a tuple.
*/
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
function getUserInfo(id: number, name: string) {
return `Идентификатор ${id}, имя ${name}`;
}
type FuncParamsType = Parameters<typeof getUserInfo>; // [id: number, name: string]
7.11. ReturnType<Type>
Данная утилита извлекает тип значения, возвращаемого функцией Type
.
/**
* Obtain the return type of a function type.
*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function getUserInfo(id: number, name: string) {
return `Идентификатор ${id}, имя ${name}`;
}
type FuncReturnType = ReturnType<typeof getUserInfo>; // string
7.12. Uppercase<StringType>
Данная утилита конвертирует строковый литеральный тип в верхний регистр.
type Method = 'get' | 'post' | 'put' | 'delete';
type UppercaseMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; // плохо
type UppercaseMethod = Uppercase<Method>; // хорошо
7.13. Lowercase<StringType>
Данная утилита конвертирует строковый литеральный тип в верхний регистр.
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
type LowercaseMethod = 'get' | 'post' | 'put' | 'delete'; // плохо
type LowercaseMethod = Lowercase<Method>; // хорошо
7.14. Capitalize<StringType>
Данная утилита конвертирует первый символ строкового литерального типа в верхний регистр.
type Method = 'get' | 'post' | 'put' | 'delete';
type CapitalizeMethod = 'Get' | 'Post' | 'Put' | 'Delete'; // плохо
type CapitalizeMethod = Capitalize<Method>; // хорошо
7.15. Uncapitalize<StringType>
Данная утилита конвертирует первый символ строкового литерального типа в верхний регистр.
type Method = 'Get' | 'Post' | 'Put' | 'Delete';
type UncapitalizeMethod = 'get' | 'post' | 'put' | 'delete'; // плохо
type UncapitalizeMethod = Uncapitalize<Method>; // хорошо
Поиск: JavaScript • TypeScript • Web-разработка • Теория • Типы данных • keyof • typeof