Привет, друзья!
Представляю вашему вниманию перевод еще нескольких статей из серии Mastering TypeScript, посвященных углубленному изучению TypeScript.
- TypeScript в деталях. Полная версия
- TypeScript в деталях. Часть 1
- TypeScript в деталях. Часть 2
- Полезные возможности современного TypeScript
- Карманная книга по TypeScript
- Шпаргалка по TypeScript
15 встроенных утилит типа
Утилиты типа (utility types) позволяют легко конвертировать, извлекать, исключать типы, получать параметры типов и типы значений, возвращаемых функциями.
1. Partial<Type>
Данная утилита делает все свойства Type
опциональными (необязательными):
/**
* Make all properties in T optional.
* typescript/lib/lib.es5.d.ts
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};
2. Required<Type>
Данная утилита делает все свойства Type
обязательными (она является противоположностью утилиты Partial
):
/**
* Make all properties in T required.
* typescript/lib/lib.es5.d.ts
*/
type Required<T> = {
[P in keyof T]-?: T[P];
};
3. Readonly<Type>
Данная утилита делает все свойства Type
доступными только для чтения (readonly
). Такие свойства являются иммутабельными (их значения нельзя изменять):
/**
* Make all properties in T readonly.
* typescript/lib/lib.es5.d.ts
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
4. Record<Keys, Type>
Данная утилита создает новый объектный тип (object type), ключами которого являются Keys
, а значениями свойств — Type
. Эта утилита может использоваться для сопоставления свойств одного типа с другим типом:
/**
* Construct a type with a set of properties K of type T.
* typescript/lib/lib.es5.d.ts
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};
5. Exclude<UnionType, ExcludedMembers>
Данная утилита создает новый тип посредством исключения из UnionType
всех членов объединения, которые могут быть присвоены (assignable) ExcludedMembers
:
/**
* Exclude from T those types that are assignable to U.
* typescript/lib/lib.es5.d.ts
*/
type Exclude<T, U> = T extends U ? never : T;
6. Extract<Type, Union>
Данная утилита создает новый тип посредством извлечения из Type
всех членов объединения, которые могут быть присвоены Union
:
/**
* Extract from T those types that are assignable to U.
* typescript/lib/lib.es5.d.ts
*/
type Extract<T, U> = T extends U ? T : never;
7. Pick<Type, Keys>
Данная утилита создает новый тип посредством извлечения из Type
набора (множества) свойств Keys
(Keys
— строковый литерал или их объединение):
/**
* From T, pick a set of properties whose keys are in the union K.
* typescript/lib/lib.es5.d.ts
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
8. Omit<Type, Keys>
Данная утилита создает новый тип посредством исключения из Type
набора свойств Keys
(Keys
— строковый литерал или их объединение) (она является противоположностью утилиты Pick
):
/**
* Construct a type with the properties of T except for those
* in type K.
* typescript/lib/lib.es5.d.ts
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
9. NonNullable<Type>
Данная утилита создает новый тип посредством исключения из Type
значений null
и undefined
:
/**
* Exclude null and undefined from T.
* typescript/lib/lib.es5.d.ts
*/
type NonNullable<T> = T extends null | undefined ? never : T;
10. Parameters<Type>
Данная утилита создает кортеж (tuple) из типов параметров функции Type
:
/**
* Obtain the parameters of a function type in a tuple.
* typescript/lib/lib.es5.d.ts
*/
type Parameters<T extends (...args: any) => any> = T extends
(...args: infer P) => any ? P : never;
11. ReturnType<Type>
Данная утилита извлекает тип значения, возвращаемого функцией Type
:
/**
* Obtain the return type of a function type.
* typescript/lib/lib.es5.d.ts
*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
12. Uppercase<StringType>
Данная утилита конвертирует строковый литеральный тип в верхний регистр:
13. Lowercase<StringType>
Данная утилита конвертирует строковый литеральный тип в нижний регистр:
14. Capitalize<StringType>
Данная утилита конвертирует первый символ строкового литерального типа в верхний регистр:
15. Uncapitalize<StringType>
Данная утилита конвертирует первый символ строкового литерального типа в нижний регистр:
Кроме описанных выше, существует еще несколько встроенных утилит типа:
ConstructorParameters<Type>
: создает кортеж или массив из конструктора функции (речь во всех случаях идет о типах). Результатом является кортеж всех параметров типа (или типnever
, еслиType
не является функцией);InstanceType<Type>
: создает тип, состоящий из типа экземпляра конструктора функции типаType
:ThisParameterType<Type>
: извлекает тип из параметраthis
функции. Если функция не имеет такого параметра, возвращаетсяunknown
.
10 особенностей классов
В объектно-ориентированных языках программирования класс — это шаблон (blueprint — проект, схема), описывающий свойства и методы, которые являются общими для всех объектов, создаваемых с помощью класса.
1. Свойства и методы
1.1. Свойства экземпляров и статические свойства
В TS
, как и в JS
, класс определяется с помощью ключевого слова class
:
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
В приведенном примере определяется класс User
с одним свойством экземпляров name
. В действительности, класс — это синтаксический сахар для функции-конструктора. Если установить результат компиляции в ES5
, то будет сгенерирован следующий код:
"use strict";
var User = /** @class */ (function () {
function User(name) {
this.name = name;
}
return User;
}());
Кроме свойств экземпляров, в классе могут определяться статические свойства. Такие свойства определяются с помощью ключевого слова static
:
class User {
static cid: string = "eft";
name: string;
constructor(name: string) {
this.name = name;
}
}
В чем разница между свойствами экземпляров и статическими свойствами? Посмотрим на компилируемый код:
"use strict";
var User = /** @class */ (function () {
function User(name) {
this.name = name;
}
User.cid = "eft";
return User;
}());
Как видим, свойства экземпляров определяются в экземпляре класса, а статические свойства — в его конструкторе.
1.2. Методы экземпляров и статические методы
Кроме свойств, в классе могут определяться методы экземпляров и статические методы:
class User {
static cid: string = "eft";
name: string;
constructor(name: string) {
this.name = name;
}
static printCid() {
console.log(User.cid);
}
send(msg: string) {
console.log(`${this.name} send a message: ${msg}`);
}
}
В чем разница между методами экземпляров и статическими методами? Посмотрим на компилируемый код:
"use strict";
var User = /** @class */ (function () {
function User(name) {
this.name = name;
}
User.printCid = function () {
console.log(User.cid);
};
User.prototype.send = function (msg) {
console.log("".concat(this.name, " send a message: ").concat(msg));
};
User.cid = "eft";
return User;
}());
Как видим, методы экземпляров добавляются в прототип конструктора, а статические методы в сам конструктор.
2. Аксессоры
В классе могут определяться так называемые аксессоры (accessors). Аксессоры, которые делятся на геттеры (getters) и сеттеры (setters) могут использоваться, например, для инкапсуляции данных или их верификации:
class User {
private _age: number = 0;
get age(): number {
return this._age;
}
set age(value: number) {
if (value > 0 && value <= 120) {
this._age = value;
} else {
throw new Error("The set age value is invalid!");
}
}
}
3. Наследование
Наследование (inheritance) — это иерархическая модель для связывания классов между собой. Наследование — это возможность класса наследовать функционал другого класса и расширять его новым функционалом. Наследование — это наиболее распространенный вид отношений между классами, между классами и интерфейсами, а также между интерфейсами. Наследование облегчает повторное использование кода.
Наследование реализуется с помощью ключевого слова extends
. Расширяемый класс называется базовым (base), а расширяющий — производным (derived). Производный класс содержит все свойства и методы базового и может определять дополнительные члены.
3.1. Базовый класс
class Person {
constructor(public name: string) {}
public say(words: string) :void {
console.log(`${this.name} says:${words}`);
}
}
3.2. Производный класс
class Developer extends Person {
constructor(name: string) {
super(name);
this.say("Learn TypeScript")
}
}
const bytefer = new Developer("Bytefer");
// "Bytefer says:Learn TypeScript"
Класс Developer
расширяет (extends) класс Person
. Следует отметить, что класс может расширять только один класс (множественное наследование в TS
, как и в JS
, запрещено):
Однако мы вполне можем реализовывать (implements) несколько интерфейсов:
interface CanSay {
say(words: string) :void
}
interface CanWalk {
walk(): void;
}
class Person implements CanSay, CanWalk {
constructor(public name: string) {}
public say(words: string) :void {
console.log(`${this.name} says:${words}`);
}
public walk(): void {
console.log(`${this.name} walk with feet`);
}
}
Рассмотренные классы являются конкретными (concrete). В TS
также существуют абстрактные (abstract) классы.
4. Абстрактные классы
Классы, поля и методы могут быть абстрактными. Класс, определенный с помощью ключевого слова abstract
, является абстрактным. Абстрактные классы не позволяют создавать объекты (другими словами, они не могут инстанцироваться (instantiate) напрямую):
Абстрактный класс — это своего рода проект класса. Подклассы (subclasses) абстрактного класса должны реализовывать всех его абстрактных членов:
class Developer extends Person {
constructor(name: string) {
super(name);
}
say(words: string): void {
console.log(`${this.name} says ${words}`);
}
}
const bytefer = new Developer("Bytefer");
bytefer.say("I love TS!"); // Bytefer says I love TS!
5. Видимость членов
В TS
для управления видимостью (visibility) свойств и методов класса применяются ключевые слова public
, protected
и private
. Видимость означает возможность доступа к членам за пределами класса, в котором они определяются.
5.1. public
Дефолтной видимостью членов класса является public
. Такие члены доступны за пределами класса без каких-либо ограничений:
class Person {
constructor(public name: string) {}
public say(words: string) :void {
console.log(`${this.name} says:${words}`);
}
}
5.2. protected
Такие члены являются защищенными. Это означает, что они доступны только в определяющем их классе, а также в производных от него классах:
class Developer extends Person {
constructor(name: string) {
super(name);
console.log(`Base Class:${this.getClassName()}`);
}
}
const bytefer = new Developer("Bytefer"); // "Base Class:Person"
5.3. private
Такие члены являются частными (приватными). Это означает, что они доступны только в определяющем их классе:
Обратите внимание: private
не делает членов по-настоящему закрытыми. Это всего лишь соглашение (как префикс _
в JS
). Посмотрим на компилируемый код:
"use strict";
var Person = /** @class */ (function () {
function Person(id, name) {
this.id = id;
this.name = name;
}
return Person;
}());
var p1 = new Person(28, "bytefer");
5.4. Частные поля
Реальные закрытые поля поддерживаются в TS
, начиная с версии 3.8
(а в JS
— с прошлого года):
Посмотрим на компилируемый код:
"use strict";
var __classPrivateFieldSet = // игнорировать соответствующий код;
var _Person_name;
class Person {
constructor(name) {
_Person_name.set(this, void 0);
__classPrivateFieldSet(this, _Person_name, name, "f");
}
}
_Person_name = new WeakMap();
const bytefer = new Person("Bytefer");
Отличия между частными и обычными полями могут быть сведены к следующему:
- закрытые поля определяются с помощью префикса
#
; - областью видимости приватного поля является определяющий его класс;
- в отношении частных полей не могут применяться модификаторы доступа (
public
и др.); - приватные поля недоступны за пределами определяющего их класса.
6. Выражение класса
Выражение класса (class expression) — это синтаксис, используемый дял определения классов. Как и функциональные выражения, выражения класса могут быть именованными и анонимными. В случае с именованными выражениями, название доступно только в теле класса.
Синтаксис выражений класса (квадратные скобки означают опциональность):
const MyClass = class [className] [extends] {
// тело класса
};
Пример определения класса Point
:
const Point = class {
constructor(public x: number, public y: number) {}
public length() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
}
const p = new Point(3, 4);
console.log(p.length()); // 5
При определении класса с помощью выражения также можно использовать ключевое слово extends
.
7. Общий класс
Для определения общего (generic) класса используется синтаксис <T, ...>
(параметры типа) после названия класса:
class Person<T> {
constructor(
public cid: T,
public name: string
) {}
}
const p1 = new Person<number>(28, "Lolo");
const p2 = new Person<string>("eft", "Bytefer");
Рассмотрим пример инстанцирования p1
:
- при создании объекта
Person
передается типnumber
и параметры конструктора; - в классе
Person
значение переменной типаT
становится числом; - наконец, параметр типа свойства
cid
в конструкторе также становится числом.
Случаи использования дженериков:
- интерфейс, функция или класс работают с несколькими типами данных;
- в интерфейсе, функции или классе тип данных используется в нескольких местах.
8. Сигнатура конструктора
При определении интерфейса для описания конструктора может использоваться ключевое слово new
:
interface Point {
new (x: number, y: number): Point;
}
new (x: number, y: number)
называется сигнатурой конструктора (construct signature). Она имеет следующий синтаксис:
ConstructSignature: new TypeParametersopt ( ParameterListopt ) TypeAnnotationopt
TypeParametersopt
, ParameterListopt
и TypeAnnotationopt
— это опциональный параметр типа, опциональный список параметров и опциональная аннотация типов, соответственно. Как применяется сигнатура конструктора? Рассмотрим пример:
interface Point {
new (x: number, y: number): Point;
x: number;
y: number;
}
class Point2D implements Point {
readonly x: number;
readonly y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const point: Point = new Point2D(1, 2); // Error
Сообщение об ошибке выглядит так:
Type 'Point2D' is not assignable to type 'Point'.
Type 'Point2D' provides no match for the signature 'new (x: number, y: number): Point'.ts(2322)
Для решения проблемы определенный ранее интерфейс Point
нужно немного отрефакторить:
interface Point {
x: number;
y: number;
}
interface PointConstructor {
new (x: number, y: number): Point;
}
Далее определяем фабричную функцию newPoint
, которая используется для создания объекта Point
, соответствующего конструктору входящего типа PointConstructor
:
class Point2D implements Point {
readonly x: number;
readonly y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
function newPoint(
pointConstructor: PointConstructor,
x: number,
y: number
): Point {
return new pointConstructor(x, y);
}
const point: Point = newPoint(Point2D, 3, 4);
9. Абстрактная сигнатура конструктора
Абстрактная сигнатура конструктора была представлена в TS 4.2
для решения таких проблем, как:
type Constructor = new (...args: any[]) => any;
abstract class Shape {
abstract getArea(): number;
}
const Ctor: Constructor = Shape; // Error
// Type 'typeof Shape' is not assignable to type 'Constructor'.
// Cannot assign an abstract constructor type to a non-abstract
// constructor type.ts(2322)
Как видим, тип абстрактного конструктора не может присваиваться типу реального конструктора. Для решения данной проблемы следует использовать абстрактную сигнатуру конструктора:
type AbstractConstructor = abstract new (...args: any[]) => any;
abstract class Shape {
abstract getArea(): number;
}
const Ctor: AbstractConstructor = Shape; // Ok
Далее определяем функцию makeSubclassWithArea
для создания подклассов класса Shape
:
function makeSubclassWithArea(Ctor: AbstractConstructor) {
return class extends Ctor {
#sideLength: number;
constructor(sideLength: number) {
super();
this.#sideLength = sideLength;
}
getArea() {
return this.#sideLength ** 2;
}
};
}
const Square = makeSubclassWithArea(Shape);
Следует отметить, что типы реальных конструкторов типам абстрактных конструкторов присваивать можно:
abstract class Shape {
abstract getArea(): number;
}
class Square extends Shape {
#sideLength: number;
constructor(sideLength: number) {
super();
this.#sideLength = sideLength;
}
getArea() {
return this.#sideLength ** 2;
}
}
const Ctor: AbstractConstructor = Shape; // Ok
const Ctor1: AbstractConstructor = Square; // Ok
В заключение кратко рассмотрим разницу между типом class
и типом typeof class
.
10. Тип class
и тип typeof class
На основе результатов приведенного примера можно сделать следующие выводы:
- при использовании класса
Person
в качестве типа значение переменной ограничивается экземпляром этого класса; - при использовании
typeof Person
в качестве типа значение переменной ограничивается статическими свойствами и методами данного класса.
Следует отметить, что в TS
используется система структурированных типов (structured type system), которая отличается от системы номинальных типов (nominal type system), применяемой в Java/C++
, поэтому следующий код в TS
будет работать без каких-либо проблем:
class Person {
constructor(public name: string) {}
}
class SuperMan {
constructor(public name: string) {}
}
const s1: SuperMan = new Person("Bytefer"); // Ok
Определение объекта с неизвестными свойствами
Приходилось ли вам сталкиваться с подобной ошибкой?
Для решения данной проблемы можно прибегнуть к помощи типа any
:
consr user: any = {}
user.id = "TS001";
user.name = "Bytefer";
Но такое решение не является безопасным с точки зрения типов и нивелирует преимущества использования TS
.
Другим решением может быть использование type
или interface
:
interface User {
id: string;
name: string;
}
const user = {} as User;
user.id = "TS001";
user.name = "Bytefer";
Кажется, что задача решена, но что если мы попробует добавить в объект свойство age
?
Property 'age' does not exist on type 'User'
Получаем сообщение об ошибке. Что делать? Когда известны типы ключей и значений, для определения типа объекта можно воспользоваться сигнатурой доступа по индексу (index signatures). Синтаксис данной сигнатуры выглядит так:
Обратите внимание: типом ключа может быть только строка, число, символ или строковый литерал. В свою очередь, значение может иметь любой тип.
Определяем тип User
с помощью сигнатуры доступа по индексу:
interface User {
id: string;
name: string;
[key: string]: string;
}
При использовании сигнатуры доступа по индексу можно столкнуться с такой ситуацией:
- Почему к соответствующему свойству можно получить доступ как с помощью строки
"1"
, так и с помощью числа1
? - Почему
keyof NumbersNames
возвращает объединение из строки и числа?
Это объясняется тем, что JS
неявно приводит число к строке при использовании первого в качестве ключа объекта. TS
применяет такой же алгоритм.
Кроме сигнатуры доступа по индексу для определения типа объекта можно использовать встроенную утилиту типа Record
. Назначение данной утилиты состоит в следующем:
type User = Record<string, string>
const user = {} as User;
user.id = "TS001"; // Ok
user.name = "Bytefer"; // Ok
В чем разница между сигнатурой доступа по индексу и утилитой Record
? Они обе могут использоваться для определения типа объекта с неизвестными (динамическими) свойствами:
const user1: Record<string, string> = { name: "Bytefer" }; // Ok
const user2: { [key: string]: string } = { name: "Bytefer" }; // Ok
Однако в случае с сигнатурой тип ключа может быть только string
, number
, symbol
или шаблонным литералом. В случае с Record
ключ может быть литералом или их объединением:
Взглянем на внутреннюю реализацию Record
:
/**
* Construct a type with a set of properties K of type T.
* typescript/lib/lib.es5.d.ts
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};
Перегрузки функции
Знаете ли вы, почему на представленном ниже изображении имеется столько определений функции ref
и зачем они нужны?
Рассмотрим пример простой функции logError
, принимающей параметр строкового типа и выводящей сообщение об ошибке в консоль инструментов разработчика в браузере:
function logError(msg: string) {
console.error(`Возникла ошибка: ${msg}`);
}
logError('Отсутствует обязательное поле.');
Что если мы хотим, чтобы данная функция также принимала несколько сообщений в виде массива?
Одним из возможных решений является использование объединения (union types):
function logError(msg: string | string[]) {
if (typeof msg === 'string') {
console.error(`Возникла ошибка: ${msg}`);
} else if (Array.isArray(msg)) {
console.error(`Возникли ошибки: ${msg.join('\n')}`);
}
}
logError('Отсутствует обязательное поле.')
logError(['Отсутствует обязательное поле.', 'Пароль должен состоять минимум из 6 символов.'])
Другим решением является использование перегрузки функции (function overloading). Перегрузка функции предполагает наличие сигнатур перегрузки (overload signatures) и сигнатуры реализации (implementation signature).
Сигнатуры перегрузки определяют типы параметров функции и тип возвращаемого ею значения, но не содержат тела функции. Функция может иметь несколько сигнатур перегрузки:
В сигнатуре реализации для типов параметров и возвращаемого значения должны использоваться более общие типы. Сигнатура реализации также должна содержать тело функции:
После объединения сигнатур перегрузки и сигнатуры реализации мы имеет такую картину:
Обратите внимание: вызываются только сигнатуры перегрузки. При обработке перегрузки функции TS
анализирует список перегрузок и пытается использовать первое определение. Если определение совпадает, анализ прекращается:
Если вызвать функцию с типом параметра, соответствующего сигнатуре реализации, возникнет ошибка:
Перегружаться могут не только функции, но и методы классов. Перегрузка метода — это техника, когда вызывается один и тот же метод класса, но с разными параметрами (разными типами параметров, разным количеством параметров, разным порядком параметров и т.д.). Конкретная сигнатура метода определяется в момент передачи реального параметра.
Рассмотрим пример перегрузки метода:
class Calculator {
add(a: number, b: number): number;
add(a: string, b: string): string;
add(a: string, b: number): string;
add(a: number, b: string): string;
add(a: string | number, b: string | number) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
}
const calculator = new Calculator();
const result = calculator.add('Bytefer', ' likes TS');
Надеюсь, что вы, как и я, нашли для себя что-то интересное.
Благодарю за внимание и happy coding!