image Привет, Хаброжители!

TypeScript — популярная надстройка над JavaScript с поддержкой статической типизации, которая наверняка покажется знакомой программистам на C# или Java. TypeScript поможет вам сократить количество ошибок и повысить общее качество кода на JavaScript.

«Основы TypeScript» — это полностью обновленное третье издание классического бестселлера Адама Фримена. В нем освещены все возможности TypeScript 5, включая новые, такие как декораторы. Сначала вы узнаете, зачем и почему был создан язык TypeScript, а затем почти сразу перейдете к практическому применению статических типов. Ничего лишнего! Каждая глава посвящена навыкам, необходимым для написания потрясающих веб-приложений.
Для кого эта книга
Эта книга предназначена для опытных программистов, которые только начинают знакомиться с TypeScript, или для тех, кто приступил к разработке веб-приложений, но столкнулся с запутанностью и непредсказуемостью JavaScript.
Структура книги
Книга состоит из трех частей. В первой рассказывается о том, как настроить среду разработки и создать простое веб-приложение, а также научиться пользоваться инструментами разработки.

Вторая часть посвящена описанию функций, с которыми вы будете сталкиваться каждый день, работая с TypeScript, включая базовые аннотации типов, типизированные функции, массивы, объекты и классы. В этой части также рассказывается о поддержке обобщенных типов в TypeScript, которые позволяют писать типо безопасный код без необходимости точно знать, какие типы будут использоваться во время выполнения программы, и декораторов — новой функции в TypeScript 5.

В заключительной части TypeScript демонстрируется в контексте создания одного веб-приложения тремя различными способами: полностью автономно и с помощью двух фреймворков — Angular и React. Здесь показано, как совместно используются функции, описанные во второй части.

Расширенные возможности обобщенных типов


В этой главе

  • Использование типизированных коллекций JavaScript с параметрами обобщенных типов.
  • Итерация по типобезопасной коллекции.
  • Создание ключей коллекции с индексными типами.
  • Преобразование типов с помощью сопоставлений.
  • Использование встроенных сопоставлений типов.
  • Выбор обобщенных типов с помощью условных выражений типа.
В этой главе продолжается описание возможностей обобщенных типов, предоставляемых TypeScript, и уделено особое внимание расширенным функциям. Здесь вы найдете информацию о том, как использовать обобщенные типы с коллекциями и итераторами, познакомитесь с индексными типами и функциями сопоставления типов. Также будет представлена наиболее гибкая из возможностей обобщенных типов — условные типы. В табл. 13.1 приведено краткое содержание главы.

В табл. 13.2 перечислены параметры компилятора TypeScript, используемые в данной главе.

image

image

Первоначальные приготовления


В этой главе мы продолжим использовать проект types, созданный ранее. Чтобы приступить к работе с материалом из данной главы, замените содержимое файла index.ts в папке src кодом из листинга 13.1.

Листинг 13.1. Замена содержимого файла index.ts из папки src

import { City, Person, Product, Employee } from "./dataTypes.js";

let products = [new Product("Running Shoes", 100), new Product("Hat", 25)];

type shapeType = { name: string };

class Collection<T extends shapeType> {
    constructor(private items: T[] = []) {}
    add(...newItems: T[]): void {
        this.items.push(...newItems);
    }

    get(name: string): T {
        return this.items.find(item => item.name === name);
    }

    get count(): number {
        return this.items.length;
    }
}

let productCollection: Collection<Product> = new Collection(products);
console.log('There are ${ productCollection.count } products');
let p = productCollection.get("Hat");
console.log('Product: ${ p.name }, ${ p.price }');

Откройте новое окно командной строки, перейдите в папку types и выполните команду, показанную в листинге 13.2, для запуска компилятора TypeScript, чтобы он автоматически выполнял код после его компиляции.

СОВЕТ
Пример проекта для этой и остальных глав книги можно загрузить с сайта github.com/manningbooks/essential-typescript-5.

Листинг 13.2. Запуск компилятора TypeScript
npm start
Компилятор скомпилирует проект, выполнит вывод, а затем перейдет в режим наблюдения, выдав следующий результат:

7:31:10 AM - Starting compilation in watch mode...
7:31:11 AM - Found 0 errors. Watching for file changes.
There are 2 products
Product: Hat, 25

Использование обобщенных коллекций


В TypeScript предусмотрена поддержка использования коллекций JavaScript с параметрами обобщенных типов, что позволяет обобщенному классу безопасно оперировать коллекциями (табл. 13.3). Классы коллекций JavaScript описаны в главе 4.

image

В листинге 13.3 показано, как обобщенный класс может использовать параметры своего типа с коллекцией.

Листинг 13.3. Использование коллекции в файле index.ts из папки src

import { City, Person, Product, Employee } from "./dataTypes.js";

let products = [new Product("Running Shoes", 100), new Product("Hat", 25)];

type shapeType = { name: string };

class Collection<T extends shapeType> {
    private items: Set<T>;

    constructor(initialItems: T[] = []) {
        this.items = new Set<T>(initialItems);
    }

    add(...newItems: T[]): void {
        newItems.forEach(newItem => this.items.add(newItem));
    }

    get(name: string): T {
        return [...this.items.values()].find(item => item.name === name);
    }

    get count(): number {
        return this.items.size;
    }
}

let productCollection: Collection<Product> = new Collection(products);
console.log('There are ${ productCollection.count } products');
let p = productCollection.get("Hat");
console.log('Product: ${ p.name }, ${ p.price }');

Для хранения элементов мы заменили класс Collection на Set, использовав параметр обобщенного типа для этой коллекции. Компилятор TypeScript использует параметр типа для предотвращения добавления в набор других типов данных, и при извлечении объектов из коллекции не требуется защита типа. Аналогичный подход можно применить и к карте (map), как показано в листинге 13.4.

Листинг 13.4. Структура данных map в файле index.ts

import { City, Person, Product, Employee } from "./dataTypes.js";

let products = [new Product("Running Shoes", 100), new Product("Hat", 25)];

type shapeType = { name: string };

class Collection<T extends shapeType> {
    private items: Map<string, T>;

    constructor(initialItems: T[] = []) {
        this.items = new Map<string, T>();
        this.add(...initialItems);
    }

    add(...newItems: T[]): void {
        newItems.forEach(newItem => this.items.set(newItem.name, newItem));
    }

    get(name: string): T {
        return this.items.get(name);
    }

    get count(): number {
        return this.items.size;
    }
}

let productCollection: Collection<Product> = new Collection(products);
console.log('There are ${ productCollection.count } products');
let p = productCollection.get("Hat");
console.log('Product: ${ p.name }, ${ p.price }');

Обобщенные классы не обязаны предоставлять параметры обобщенного типа для коллекций. Вместо этого можно указывать конкретные типы. В приведенном примере для хранения объектов используется Map, где свойство name выступает в качестве ключа. Безопасность применения свойства name обеспечивается ограничением, наложенным на параметр типа с именем T. Код в листинге 13.4 выдает следующий результат:

There are 2 products
Product: Hat, 25

Использование обобщенных итераторов


Как объяснялось в главе 4, итераторы позволяют перечислять последовательность значений. Поддержка итераторов является общей функцией для классов, работающих с другими типами, например коллекциями. В табл. 13.4 перечислены интерфейсы TypeScript, предназначенные для описания итераторов и их результатов.

image

В листинге 13.5 показано применение интерфейсов Iterator и Ite ratorResult для предоставления доступа к содержимому Map<string, T>, используемого для хранения объектов класса Collection.

Листинг 13.5. Итерация объектов в файле index.ts из папки src

import { City, Person, Product, Employee } from "./dataTypes.js";

let products = [new Product("Running Shoes", 100), new Product("Hat", 25)];

type shapeType = { name: string };

class Collection<T extends shapeType> {
    private items: Map<string, T>;

    constructor(initialItems: T[] = []) {
        this.items = new Map<string, T>();
        this.add(...initialItems);
    }

    add(...newItems: T[]): void {
        newItems.forEach(newItem => this.items.set(newItem.name, newItem));
    }

    get(name: string): T {
        return this.items.get(name);
    }

    get count(): number {
        return this.items.size;
    }

    values(): Iterator<T> {
        return this.items.values();
    }
}

let productCollection: Collection<Product> = new Collection(products);
console.log('There are ${ productCollection.count } products');

let iterator: Iterator<Product> = productCollection.values();
let result: IteratorResult<Product> = iterator.next();
while (!result.done) {
    console.log('Product: ${result.value.name}, ${ result.value.price}');
    result = iterator.next();
}

Метод values, определенный классом Collection, возвращает итератор Iterator. Когда этот метод вызывается для объекта Collection, возвращаемый им итератор будет создавать объекты IteratorResult с помощью своего метода next. Свойство result каждого объекта IteratorResult будет возвращать Product, что позволит выполнять итерацию объектов, управляемых коллекцией. Код в листинге 13.5 выдает следующий результат:

There are 2 products
Product: Running Shoes, 100
Product: Hat, 25

ИСПОЛЬЗОВАНИЕ ИТЕРАТОРОВ В JAVASCRIPT ES5 И БОЛЕЕ РАННИХ ВЕРСИЯХ
Итераторы были введены в стандарте JavaScript ES6. Если вы используете итераторы в своем проекте и ориентируетесь на более ранние версии JavaScript, то необходимо установить свойство компилятора TypeScript downlevelIteration в значение true.

Объединение итерируемого и итератора


Интерфейс IterableIterator допускается использовать для описания объектов, которые могут быть итерированы и которые также определяют свойство Symbol.iterator. Объекты, реализующие этот интерфейс, можно перечислить более элегантно (листинг 13.6).

Листинг 13.6. Использование итератора в файле index.ts

import { City, Person, Product, Employee } from "./dataTypes.js";

let products = [new Product("Running Shoes", 100), new Product("Hat", 25)];

type shapeType = { name: string };

class Collection<T extends shapeType> {

    private items: Map<string, T>;

    constructor(initialItems: T[] = []) {
        this.items = new Map<string, T>();
        this.add(...initialItems);
    }

    add(...newItems: T[]): void {
        newItems.forEach(newItem => this.items.set(newItem.name, newItem));
    }

    get(name: string): T {
        return this.items.get(name);
    }
 
    get count(): number {
        return this.items.size;
    }

    values(): IterableIterator<T> {
        return this.items.values();
    }
}

let productCollection: Collection<Product> = new Collection(products);
console.log('There are ${ productCollection.count } products');
[...productCollection.values()].forEach(p =>
    console.log('Product: ${p.name}, ${ p.price}'));

Метод values возвращает объект IterableIterator. Это возможно благодаря тому, что результат метода Mapопределяет все члены, заданные интерфейсом. Комбинированный интерфейс позволяет напрямую итерировать результат метода values. В приведенном примере с помощью оператора spread заполняется массив, а затем перечисляется его содержимое с помощью метода forEach. Код в листинге 13.6 выдает следующий результат:

There are 2 products
Product: Running Shoes, 100
Product: Hat, 25

Создание итерируемого класса


Классы, определяющие свойство Symbol.iterator, могут реализовать интерфейс
Iterable, который позволяет выполнять итерацию без необходимости вызова метода или чтения свойства, как показано в листинге 13.7.

Листинг 13.7. Создание итерируемого класса в файле index.ts

import { City, Person, Product, Employee } from "./dataTypes.js";

let products = [new Product("Running Shoes", 100), new Product("Hat", 25)];

type shapeType = { name: string };

class Collection<T extends shapeType> implements Iterable<T> {
    private items: Map<string, T>;

    constructor(initialItems: T[] = []) {
        this.items = new Map<string, T>();
        this.add(...initialItems);
    }

    add(...newItems: T[]): void {
        newItems.forEach(newItem => this.items.set(newItem.name, newItem));
    }

    get(name: string): T {
        return this.items.get(name);
    }

    get count(): number {
        return this.items.size;
    }

     [Symbol.iterator](): Iterator<T> {
        return this.items.values();
    }
}

let productCollection: Collection<Product> = new Collection(products);
console.log('There are ${ productCollection.count } products');

[...productCollection].forEach(p =>
    console.log('Product: ${p.name}, ${ p.price}'));

Новое свойство реализует интерфейс Iterable, что указывает на то, что оно определяет свойство Symbol.iterator, возвращающее объект Iterator, который может быть использован для итерации. Код в листинге 13.7 выдает следующий результат:

There are 2 products
Product: Running Shoes, 100
Product: Hat, 25

Индексные типы


Класс Collection имеет ограничения на принимаемые им типы благодаря структуре типа. Это гарантирует, что все объекты, с которыми он работает, имеют свойство name, которое можно использовать в качестве ключа для хранения и извлечения объектов в Map.

TypeScript предоставляет набор связанных функций, позволяющих использовать в роли ключа любое свойство, определенное объектом, сохраняя при этом типобезопасность. Такие возможности сложны для понимания, поэтому мы рассмотрим, как они работают изолированно, а затем используем их для улучшения класса Collection.

Запрос индексного типа


Ключевое слово keyof, также известное как оператор запроса индексного типа, возвращает объединение имен свойств типа с помощью функциональности типа с литеральными значениями, описанной в главе 9. В листинге 13.8 показано применение keyof к классу Product.

Листинг 13.8. Использование оператора запроса индексного типа в файле index.ts

import { City, Person, Product, Employee } from "./dataTypes.js";

let myVar: keyof Product = "name";
myVar = "price";
myVar = "someOtherName";

Аннотацией типа для переменной myVar является keyof Product, которая будет объединением имен свойств, определенных классом Product. Следовательно, переменной myVar могут быть присвоены только строковые значения name и price, поскольку это имена единственных двух свойств, определенных классом Product в файле dataTypes.ts, созданном в главе 12:

...
export class Product {
    constructor(public name: string, public price: number) {}
}
...

Попытка присвоить любое другое значение переменной myVar, как это делает
заключительный оператор в листинге 13.8, приведет к ошибке компилятора:

src/index.ts(5,1): error TS2322: Type '"someOtherName"' is not assignable
to type 'keyof Product'.

Ключевое слово keyof можно использовать для ограничения параметров обобщенного типа, чтобы они могли иметь только тип, соответствующий свойствам другого типа, как показано в листинге 13.9.

Листинг 13.9. Ограничение параметра обобщенного типа в файле index.ts

import { City, Person, Product, Employee } from "./dataTypes.js";

function getValue<T, K extends keyof T>(item: T, keyname: K) {
    console.log('Value: ${item[keyname]}');
}

let p = new Product("Running Shoes", 100);
getValue(p, "name");
getValue(p, "price");

let e = new Employee("Bob Smith", "Sales");
getValue(e, "name");
getValue(e, "role");

В данном примере определена функция getValue, у которой параметр типа Kограничен с помощью typeof T. Это означает, что K может быть именем только одного из свойств, определяемых T, независимо от типа, используемого для T при вызове функции. Когда функция getValue используется с объектом Product, параметр keyname может быть только name или price. А когда функция getValue используется с объектом Employee, параметр keyname может быть только nameили role. В обоих случаях параметр keyname может быть использован для безопасного получения или установки значения соответствующего свойства из объекта Product или Employee. Код в листинге 13.9 выдает следующий результат:

Value: Running Shoes
Value: 100
Value: Bob Smith
Value: Sales

Явное указание параметров обобщенных типов для индексных типов


В листинге 13.9 метод getValue был вызван без указания аргументов обобщенного типа, что позволило компилятору вывести типы из аргументов функции. Явное указание аргументов типа раскрывает один из аспектов использования оператора запроса индексного типа, который может сбить с толку (листинг 13.10).

Листинг 13.10. Использование явных аргументов типа в файле index.ts

import { City, Person, Product, Employee } from "./dataTypes.js";

function getValue<T, K extends keyof T>(item: T, keyname: K) {
    console.log('Value: ${item[keyname]}');
}

let p = new Product("Running Shoes", 100);
getValue<Product, "name">(p, "name");
getValue(p, "price");

let e = new Employee("Bob Smith", "Sales");
getValue(e, "name");
getValue(e, "role");

Может показаться, что свойство, необходимое для примера, указано дважды, но name имеет два разных назначения в модифицированном операторе, как показано на рис. 13.1.

image

В качестве аргумента обобщенного типа name представляет собой тип с литеральным значением, который указывает на один из типов Keyof Product и используется компилятором TypeScript для проверки типов. А в роли аргумента функции name представляет собой значение типа string, которое используется средой выполнения JavaScript при выполнении кода. Код в листинге 13.10 выдает следующий результат:

Value: Running Shoes
Value: 100
Value: Bob Smith
Value: Sales

Оператор индексированного доступа


Оператор индексированного доступа используется для извлечения типа одного или нескольких свойств, как показано в листинге 13.11.

Листинг 13.11. Использование оператора индексированного доступа в файле index.ts

import { City, Person, Product, Employee } from "./dataTypes.js";

function getValue<T, K extends keyof T>(item: T, keyname: K) {
    console.log('Value: ${item[keyname]}');
}

type priceType = Product["price"];
type allTypes = Product[keyof Product];

let p = new Product("Running Shoes", 100);
getValue<Product, "name">(p, "name");
getValue(p, "price");

let e = new Employee("Bob Smith", "Sales");
getValue(e, "name");
getValue(e, "role");

Оператор индексированного доступа выражается с помощью квадратных скобок после типа. Например, Product["price"] — это число, поскольку именно такой тип имеет свойство price, определяемое классом Product. Оператор индексированного доступа работает с литеральными типами значений, поэтому его можно использовать в запросах индексного типа, например, так:

...
type allTypes = Product[keyof Product];
...

Выражение keyof Product возвращает объединение типов литеральных значений с именами свойств, определенных в классе Product, — "name" | "price". Оператор индексированного доступа возвращает объединение типов этих свойств, например Product[keyof Product] — string | number, что является объединением типов свойств name и price.

СОВЕТ
Типы, возвращаемые оператором индексированного доступа, называются типами поиска.

Оператор индексированного доступа чаще всего используется с обобщенными типами, что позволяет безопасно работать с типами свойств, даже если конкретные типы будут неизвестны (листинг 13.12).

Листинг 13.12. Использование оператора индексированного доступа в файле index.ts

import { City, Person, Product, Employee } from "./dataTypes.js";

function getValue<T, K extends keyof T>(item: T, keyname: K): T[K] {
    return item[keyname];
}

let p = new Product("Running Shoes", 100);
console.log(getValue<Product, "name">(p, "name"));
console.log(getValue(p, "price"));

let e = new Employee("Bob Smith", "Sales");
console.log(getValue(e, "name"));
console.log(getValue(e, "role"));

Оператор индексированного доступа выражается с помощью обычного типа, его типа keyof и квадратных скобок, как показано на рис. 13.2.

image

Оператор индексированного доступа T[K] из листинга 13.12 сообщает компилятору, что результат функции getValue будет иметь тип свойства, имя которого указано в аргументе типа keyof. Это позволяет компилятору самому определить тип результата на основе аргументов обобщенного типа, используемых при вызове функции. Для объекта Product это означает, что аргумент name приведет к строковому результату, а price — к числовому. Код в листинге 13.12 выдает следующий результат:

Running Shoes
100
Bob Smith
Sales 

Использование индексного типа для класса collection


Использование индексного типа позволяет модифицировать класс таким образом, чтобы он мог хранить объекты любого типа, а не только те, которые определяют свойство name. В листинге 13.13 показаны изменения класса, в котором с помощью запроса индексного типа свойство конструктора propertyName ограничено именами свойств, определяемых параметром обобщенного типа T, обеспечивающим ключ, по которому объекты могут храниться в Map.

Листинг 13.13. Использование индексного типа в классе collection в файле index.ts

import { City, Person, Product, Employee } from "./dataTypes.js";

let products = [new Product("Running Shoes", 100), new Product("Hat", 25)];

//type shapeType = { name: string };

class Collection<T, K extends keyof T> implements Iterable<T> {
    private items: Map<T[K], T>;

    constructor(initialItems: T[] = [], private propertyName: K) {
        this.items = new Map<T[K], T>();
        this.add(...initialItems);
    }

    add(...newItems: T[]): void {
        newItems.forEach(newItem =>
            this.items.set(newItem[this.propertyName], newItem));
    }

    get(key: T[K]): T {
        return this.items.get(key);
    }

    get count(): number {
        return this.items.size;
    }

     [Symbol.iterator](): Iterator<T> {
        return this.items.values();
    }
}

let productCollection: Collection<Product, "name">
    = new Collection(products, "name");
console.log('There are ${ productCollection.count } products');

let itemByKey = productCollection.get("Hat");
console.log('Item: ${ itemByKey.name}, ${ itemByKey.price}');

Класс был переписан с дополнительным параметром обобщенного типа K, который ограничивается keyof T — типом данных объектов, хранящихся в коллекции. Новый экземпляр Collection<T, K> создается следующим образом:

...
let productCollection: Collection<Product, "name">
    = new Collection(products, "name");
...

Результат выполнения кода из листинга 13.13:

There are 2 products
Item: Hat, 25

Частые цепочки угловых и квадратных скобок в листинге 13.13 могут быть сложны для понимания, если вы только начинаете использовать индексные типы. Чтобы облегчить понимание кода, в табл. 13.5 описаны важные параметры типа, конструктора и соответствующие типы, в которые они разрешаются для объекта Collection<Product, «name»>, созданного в примере.

image

В результате использования индексного типа в листинге 13.13 становится возможным хранение объектов с использованием любого свойства, а также сохранение объектов различных типов. В листинге 13.14 изменен способ создания класса Collection<T, K> таким образом, что теперь в качестве ключа используется свойство price. В листинге также опущены аргументы обобщенных типов, и компилятор может самостоятельно определить необходимые типы.

Листинг 13.14. Изменение ключевого свойства в файле index.ts

...
let productCollection = new Collection(products, "price");
console.log('There are ${ productCollection.count } products');

let itemByKey = productCollection.get(100);
console.log('Item: ${ itemByKey.name}, ${ itemByKey.price}');
...

Тип аргумента метода get меняется в соответствии с типом свойства ключа, что позволяет получать объекты с помощью аргумента number. Код в листинге 13.14 выдает следующий результат:

There are 2 products
Item: Running Shoes, 100

Сопоставление типа


Сопоставленные типы создаются путем применения преобразования к свойствам существующего типа. Лучший способ понять, как это работает, — создать тип, который обрабатывает тип, но не изменяет его (листинг 13.15).

Листинг 13.15.Использование сопоставленного типа в файле index.ts

import { City, Person, Product, Employee } from "./dataTypes.js";

type MappedProduct = {
     [P in keyof Product] : Product[P]
};

let p: MappedProduct = { name: "Kayak", price: 275};
console.log('Mapped type: ${p.name}, ${p.price}');

Сопоставление типов представляет собой выражение, в котором выбираются имена свойств, включаемых в сопоставленный тип, и указывается тип для каждого из них, как показано на рис. 13.3.

image

Селектор имени свойства определяет параметр типа, названный в данном примере P, и использует ключевое слово in для перечисления типов в объединении литеральных значений. Объединение типов может быть выражено напрямую, например "name"|"price", или получено с помощью keyof.

Компилятор TypeScript создает новое свойство в сопоставленном типе для всех типов в объединении. Тип каждого свойства определяется селектором типа, который можно получить из исходного типа с помощью оператора индексированного доступа с указанием P в качестве типа с литеральным значением для поиска.

Тип MappedProduct в листинге 13.15 использует keyofдля выбора свойств, определенных классом Product, и оператор индексированного типа для получения типа каждого из этих свойств. Результат эквивалентен данному типу:

type MappedProduct = {
    name: string;
    price: number;
}

Код в листинге 13.15 выдает следующий результат:

 Mapped type: Kayak, 275

Изменение имен и типов сопоставления


В предыдущем примере в процессе сопоставления сохранялись имена и типы свойств. Однако сопоставление типов поддерживает изменение как имени, так и типа свойств в новом типе, как показано в листинге 13.16.

Листинг 13.16. Изменение имен и типов в файле index.ts

import { City, Person, Product, Employee } from "./dataTypes.js";

type MappedProduct = {
     [P in keyof Product] : Product[P]
};

let p: MappedProduct = { name: "Kayak", price: 275};
console.log('Mapped type: ${p.name}, ${p.price}');

type AllowStrings = {
     [P in keyof Product] : Product[P] | string
}
let q: AllowStrings = { name: "Kayak", price: "apples" };
console.log('Changed type # 1: ${q.name}, ${q.price}');

type ChangeNames = {
     [P in keyof Product as '${P}Property'] : Product[P]
}

let r: ChangeNames = { nameProperty: "Kayak", priceProperty: 12 };
console.log('Changed type # 2: ${r.nameProperty}, ${r.priceProperty}');

Тип AllowStrings создается с помощью сопоставления, которое создает объединение типов между строкой и исходным типом свойства, например, так:

...
[P in keyof Product] : Product[P] | string
...

В результате получается тип, эквивалентный данному типу:

type AllowStrings = {
    name: string;
    price: number | string;
}

Тип ChangeNames создается с сопоставлением, которое изменяет имя каждого
свойства путем добавления Property.

...
[P in keyof Product as '${P}Property'] : Product[P]
...

Ключевое слово as сочетается с выражением, определяющим имя свойства. В этом случае для модификации существующего имени используется строка-шаблон, результат которой эквивалентен следующему типу:

type ChangeNames = {
    nameProperty: string;
    priceProperty: number;
}

Код в листинге 13.16 при компиляции и выполнении выдаст следующий результат:

Mapped type: Kayak, 275
Changed type # 1: Kayak, apples
Changed type # 2: Kayak, 12

Параметр обобщенного типа с сопоставленным типом


Сопоставленные типы полезнее, когда они определяют параметр обобщенного типа (листинг 13.17), что позволяет применять описываемое ими преобразование к более широкому диапазону типов.

Листинг 13.17. Использование параметра обобщенного типа в файле index.ts

import { City, Person, Product, Employee } from "./dataTypes.js";

type Mapped<T> = {
     [P in keyof T] : T[P]
};

let p: Mapped<Product> = { name: "Kayak", price: 275};
console.log('Mapped type: ${p.name}, ${p.price}');

let c: Mapped<City> = { name: "London", population: 8136000};
console.log('Mapped type: ${c.name}, ${c.population}');

Тип Mapped определяет параметр обобщенного типа T, который является типом, подлежащим преобразованию. Параметр типа используется в селекторах имени и типа, то есть любой тип может быть сопоставлен с помощью параметра обобщенного типа. В листинге 13.17 сопоставленный тип Mapped используется для классов Product и City и выводит следующий результат:

Mapped type: Kayak, 275
Mapped type: London, 8136000

Изменение обязательности и изменчивости свойств


Сопоставленные типы могут изменять свойства, делая их необязательными или обязательными, а также добавлять или удалять ключевое слово readonly, как показано в листинге 13.18.

Листинг 13.18. Изменение свойств в файле index.ts

import { City, Person, Product, Employee } from "./dataTypes.js";

type MakeOptional<T> = {
     [P in keyof T]? : T[P]
};

type MakeRequired<T> = {
     [P in keyof T]-? : T[P]
};

type MakeReadOnly<T> = {
    readonly [P in keyof T] : T[P]
};

type MakeReadWrite<T> = {
    -readonly [P in keyof T] : T[P]
};

type optionalType = MakeOptional<Product>;
type requiredType = MakeRequired<optionalType>;
type readOnlyType = MakeReadOnly<requiredType>;
type readWriteType = MakeReadWrite<readOnlyType>;

let p: readWriteType = { name: "Kayak", price: 275};
console.log('Mapped type: ${p.name}, ${p.price}');

Чтобы сделать свойства сопоставленного типа необязательными, после селектора имени ставится символ ?, а символы -? используются для того, чтобы сделать свойства обязательными. Свойствам присваивается статус «только для чтения» и «для чтения и записи», для чего перед селектором имен пишутся ключевые слова readonly и -readonly соответственно.

Сопоставленные типы изменяют все свойства, определяемые исходным типом, так что, например, тип, полученный с помощью при применении к классу Product, эквивалентен типу:

type optionalType = {
    name?: string;
    price?: number;
}

Типы, созданные сопоставлениями, могут быть переданы другим сопоставлениям, создавая таким образом цепочку преобразований. В листинге тип, полученный с помощью сопоставления , затем преобразуется сопоставлением , результат которого подается на вход сопоставлению , а затем сопоставлению . В итоге свойства сначала делаются необязательными, затем обязательными, потом только для чтения и, наконец, доступными для записи. Код из листинга 13.18 выдает следующее:

Mapped type: Kayak, 275

Основные встроенные сопоставления


TypeScript предоставляет встроенные сопоставленные типы, некоторые из которых соответствуют преобразованиям, приведенным в листинге 13.18, а некоторые описаны в последующих разделах. В табл. 13.6 описаны основные встроенные отображения.

image

Встроенной команды для удаления ключевого слова readonly не существует, но в листинге 13.19 сопоставления заменены теми, которые предоставляет Type Script.

Листинг 13.19. Использование встроенных команд сопоставления в файле index.ts

import { City, Person, Product, Employee } from "./dataTypes.js";

// type MakeOptional<T> = {
//     [P in keyof T]? : T[P]
// };

// type MakeRequired<T> = {
//     [P in keyof T]-? : T[P]
// };

// type MakeReadOnly<T> = {
//     readonly [P in keyof T] : T[P]
// };

type MakeReadWrite<T> = {
    -readonly [P in keyof T] : T[P]
};

type optionalType = Partial<Product>;
type requiredType = Required<optionalType>;
type readOnlyType = Readonly<requiredType>;
type readWriteType = MakeReadWrite<readOnlyType>;
let p: readWriteType = { name: "Kayak", price: 275};
console.log('Mapped type: ${p.name}, ${p.price}');

Сопоставления из табл. 13.6 имеют тот же эффект, что и определенные в листинге 13.19, и код в листинге 13.19 выдает следующий результат:

Mapped type: Kayak, 275

Сопоставление определенных свойств

Запрос индексного типа для сопоставленного типа можно выразить в виде параметра обобщенного типа, который затем допускается использовать для выбора конкретных свойств для сопоставления по имени, как показано в листинге 13.20.

Листинг 13.20. Выбор определенных свойств в файле index.ts

import { City, Person, Product, Employee } from "./dataTypes.js";

type SelectProperties<T, K extends keyof T> = {
     [P in K]: T[P]
};

let p1: SelectProperties<Product, "name"> = { name: "Kayak" };
let p2: Pick<Product, "name"> = { name: "Kayak" };
let p3: Omit<Product, "price"> = { name: "Kayak"};
console.log('Custom mapped type: ${p1.name}');
console.log('Built-in mapped type (Pick): ${p2.name}');
console.log('Built-in mapped type (Omit): ${p3.name}');

Сопоставление SelectProperties определяет дополнительный параметр обобщенного типа K, который ограничен ключевым словом keyof таким образом, чтобы можно было указать только типы, соответствующие свойствам, определенным параметром типа T. Параметр нового типа используется в селекторе имен сопоставления, что позволяет выбирать отдельные свойства для включения в сопоставленный тип, например, так:

...
let p1: SelectProperties<Product, "name"> = { name: "Kayak" };
...

Это сопоставление выбирает свойство name, определенное классом Product. Несколько свойств можно выразить в виде объединения типов, и TypeScript предоставляет встроенное сопоставление Pick<T, K>, выполняющее ту же функцию.

...
let p2: Pick<Product, "name"> = { name: "Kayak" };
...

Сопоставление Pickуказывает ключи, которые должны храниться в сопоставленном типе. Сопоставление Omit работает противоположным образом и исключает один или несколько ключей.

...
let p3: Omit<Product, "price"> = { name: "Kayak"};
...

Результат всех трех сопоставлений одинаков, и код из листинга 13.20 выдает следующий результат:

Custom mapped type: Kayak
Built-in mapped type (Pick): Kayak
Built-in mapped type (Omit): Kayak

Комбинирование преобразований в одном сопоставлении


В листинге 13.19 показано, как можно комбинировать сопоставления для создания цепочки преобразований. Но при этом сопоставления могут применять несколько изменений к свойствам, как показано в листинге 13.21.

Листинг 13.21. Комбинирование преобразований в файле index.ts

import { City, Person, Product, Employee } from "./dataTypes.js";

type CustomMapped<T, K extends keyof T> = {
    readonly[P in K]?: T[P]
};

type BuiltInMapped<T, K extends keyof T> = Readonly<Partial<Pick<T, K>>>;

let p1: CustomMapped<Product, "name"> = { name: "Kayak" };
let p2: BuiltInMapped<Product, "name"| "price">
    = { name: "Lifejacket", price: 48.95};
console.log('Custom mapped type: ${p1.name}');
console.log('Built-in mapped type: ${p2.name}, ${p2.price}');

Для сопоставлений пользовательских типов в одном преобразовании допускается использовать вопросительный знак (?) и ключевое слово readonly, которое может быть ограничено, чтобы разрешить выбор свойства по имени. Сопоставления также могут быть объединены в цепочку, как показано на примере комбинации сопоставлений Pick, partial и Readonly. Код, приведенный в листинге 13.21, выдает следующий результат:

Custom mapped type: Kayak
Built-in mapped type: Lifejacket, 48.95
Об авторе
Адам Фримен (Adam Freeman) — опытный профессионал в области IT, занимавший руководящие должности во многих компаниях. До недавнего времени он занимал посты технического директора и главного инженера в одном из крупнейших банков. Сейчас Адам посвящает свое время в основном написанию книг и бегу на длинные дистанции.

Более подробно с книгой можно ознакомиться на сайте издательства:

» Оглавление
» Отрывок

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Для Хаброжителей скидка 25% по купону — TypeScript1

Комментарии (7)


  1. alex_k777
    06.06.2024 16:19
    +2

    Я купил! Спасибо! Классная книжка


  1. adron_s
    06.06.2024 16:19

    А на английском этой же книги нет?