TypeScript. Начало работы, часть 5 из 7

03.04.2023

Теги: JavaScriptTypeScriptWeb-разработкаТеорияТипыДанных

Основы TypeScript

13. Объекты и типы

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

let person: { name: string; age: number } = { name: 'Сергей', age: 32 };

Некоторые свойства объекта можно сделать необязательными.

let person: { name: string; age?: number };
person = { name: 'Сергей', age: 32 };
person = { name: 'Николай' };
console.log(person.age); // undefined

Интерфейс задает набор полей с указанием типа, которые должен содержать объект, реализующий этот интерфейс.

interface Person {
    id: number;
    name: string;
    email: string;
    age?: number;
}
// объект person реализует интерфейс Person
const person: Person = {
    id: 12345,
    name: 'Сергей Иванов',
    email: 'ivanov@mail.ru',
    age: 32,
};

Псевдоним типа похож на интерфейс, но имеет некоторые отличия.

type Person = {
    id: number;
    name: string;
    email: string;
    age?: number;
};
const person: Person = {
    id: 12345,
    name: 'Сергей Иванов',
    email: 'ivanov@mail.ru',
    age: 32,
};

Псевдоним типа и интерфейс можно расширять, добавляя новые поля.

interface Person {
    id: number;
    name: string;
    email: string;
    age?: number;
}
const person: Person = {
    id: 12345,
    name: 'Сергей Иванов',
    email: 'ivanov@mail.ru',
    age: 32,
};

interface Employee extends Person {
    position: string;
}
const employee: Employee = {
    id: 12345,
    name: 'Сергей Иванов',
    email: 'ivanov@mail.ru',
    age: 32,
    position: 'developer',
};
type Person = {
    id: number;
    name: string;
    email: string;
    age?: number;
};
const person: Person = {
    id: 12345,
    name: 'Сергей Иванов',
    email: 'ivanov@mail.ru',
    age: 32,
};

type Employee = Person & {
    position: string;
};
const employee: Employee = {
    id: 12345,
    name: 'Сергей Иванов',
    email: 'ivanov@mail.ru',
    age: 32,
    position: 'developer',
};

В объявленный интерфейс можно добавлять новые поля, в объявленный псевдоним типа — нельзя.

interface Person {
    id: number;
    name: string;
    email: string;
    age?: number;
}

interface Person {
    position: string;
}
type Person = {
    id: number;
    name: string;
    email: string;
    age?: number;
};

type Person = { // Error: Duplicate identifier Person
    position: string;
};

Объект может быть сложным, содержать не только примитивы, но и вложенные объекты.

interface DateTime {
    day: number;
    month: number;
    year: number;
    hour: number;
    minute: number;
}

interface Passport {
    id: string;
    name: string;
    citizenship: string;
}

interface Ticket {
    seat: string;
    expiration: DateTime;
}

interface TravelDocument {
    passport: Passport;
    ticket: Ticket;
}

Функции и массивы тоже являются объектами и могут реализовать интерфейсы.

interface FullNameBuilder {
    (name: string, surname: string): string;
}

const fullNameBuilderOne: FullNameBuilder = function (
    name: string,
    surname: string
): string {
    return name + ' ' + surname;
};

const fullNameBuilderTwo: FullNameBuilder = (
    name: string,
    surname: string
): string => name + ' ' + surname;
interface StringArray {
    [index: number]: string;
}

const phones: StringArray = ['iPhone 7', 'HTC 10', 'HP Elite x3'];

interface Dictionary {
    [index: string]: string;
}

const colors: Dictionary = {};
colors.red = '#ff0000';
colors.green = '#00ff00';
colors.blue = '#0000ff';

14. Приведение к типу

В некоторых ситуациях переменная может представлять какой-то широкий тип — например, any или number|string. Однако при этом, нам нужно использовать переменную как значение строго определенного типа — например, как string или number. И в этом случае мы можем привести переменную к нужному типу — например, к string или number.

let uniqueId: string | number;

uniqueId = '9df149f1af8eb62e72414b8b32aa0347';
const stringId = uniqueId as string;
console.log(typeof stringId); // string

uniqueId = 12345;
const numberId = uniqueId as number;
console.log(typeof numberId); // number
let uniqueId: string | number;

uniqueId = '9df149f1af8eb62e72414b8b32aa0347';
const stringId = <string>uniqueId;
console.log(typeof stringId); // string

uniqueId = 12345;
const numberId = <number>uniqueId;
console.log(typeof numberId); // number

Допустим, у нас есть объект, который был объявлен без каких-либо свойств. Но мы можем привести его к типу объекта со свойствами.

const person: object = {};
person.name = 'Сергей'; // ошибка
person.age = 32; // ошибка
interface Person {
    name: string;
    age: number;
}

const person = {} as Person;
person.name = 'Сергей';
person.age = 32;

15. Обобщенные типы

Обобщённый тип (generic type) — это заглушка, которая резервирует место для конкретного типа, который будет передан при вызове функции или метода класса.

Первый пример

Допустим, у нас есть функция, принимающая на вход один аргумент и возвращающая его же без каких-либо изменений. Такая функция присутствует во многих библиотеках функционального программирования. Посмотрим на простейшую реализацию такой функции с использованием any.

function identity(arg: any): any {
    return arg;
}

Всё как в описании — функция принимает аргумент любого типа и возвращает его же. Однако, это не совсем так. В нашей реализации функция принимает аргумент какого-то типа и возвращает значение какого-то типа — но при этом они никак не связаны. Грубо говоря, сейчас мы можем передать аргумент типа number и получить значение типа string — это валидно, потому что any подразумевает что угодно.

Давайте перепишем функцию так, чтобы было понятно, что тип переданного аргумента является и типом возвращаемого значения.

function identity<Type>(arg: Type): Type {
    return arg;
}

Здесь <Type> – это некий параметр типа, который резервирует место для настоящего типа. Этот настоящий тип станет известен только в момент вызова функции. В этот момент компилятор подставляет вместо Type настоящий тип — например number или string. После этого валидирует типы переданных аргументов, тело функции и тип возвращаемого значения.

Здесь напрашивается аналогия с параметрами и аргументами функции — параметры заменяются аргументами и начинается выполнение тела функции. Но здесь в качестве аргумента передается не значение (например, 12345), а тип (например, number).

Попробуем вызвать эту функцию с аргументом типа number и проследим за тем, как работает компилятор.

const value = identity<number>(115);
// в момент вызова параметр типа Type заменяется настоящим типом number
function identity<number>(arg: number): number {
    return arg;
}

Давайте усложним функцию identity — пусть она выводит в консоль длину переданного аргумента.

function loggingIdentity<Type>(arg: Type): Type {
    console.log(arg.length); // Property «length» does not exist on type «Type»
    return arg;
}

Теперь комппилятор выдает ошибку, потому что мы обращаемся к свойству length, но нигде не сказали, что у arg есть это свойство. Параметр Type может быть любого типа — и у этого типа может не оказаться свойства length. А вот с массивом это будет работать — потому что у массива это свойство точно есть.

function loggingIdentity<Type>(arg: Type[]): Type[] {
    console.log(arg.length);
    return arg;
}

const data: number[] = [1, 2, 3, 4, 5];
loggingIdentity<number>(data);
function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
    console.log(arg.length);
    return arg;
}

const data: number[] = [1, 2, 3, 4, 5];
loggingIdentity<number>(data);

Чтобы это работало, как было задумано — нужно потребовать, чтобы у Type было свойство length.

interface HasLength {
    length: number;
}

function loggingIdentity<Type extends HasLength>(arg: Type): Type {
    console.log(arg.length);
    return arg;
}

const dataNumbers: number[] = [1, 2, 3, 4, 5];
loggingIdentity<number[]>(dataNumbers);

const dataString: string = 'generic type';
loggingIdentity<string>(dataString);

Второй пример

Рассмотрим еще один пример. Функция appendIdentifier принимает любой объект и возвращает объект со всеми полями, которые были у входного объекта, добавляя ему поле id со случайным значением от 0 до 1000.

const appendIdentifier = (obj: object) => {
    const id = Math.floor(Math.random() * 1000);
    return { ...obj, id };
};

const person = appendIdentifier({ name: 'Сергей', age: 40 });

console.log(person.id); // 271
console.log(person.name); // Property «name» does not exist on type «{ id: number; }»

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

const appendIdentifier = <Type>(obj: Type) => {
    const id = Math.floor(Math.random() * 1000);
    return { ...obj, id };
};

const person = appendIdentifier({ name: 'Сергей', age: 40 });

console.log(person.id); // 271
console.log(person.name); // Сергей

Однако теперь у нас новая проблема — мы может передать в appendIdentifier все что угодно, и компилятор не видит в этом проблемы.

const appendIdentifier = <Type>(obj: Type) => {
    const id = Math.floor(Math.random() * 1000);
    return { ...obj, id };
};

const personOne = appendIdentifier({ name: 'Сергей', age: 40 });
console.log(personOne.id); // 271
console.log(personOne.name); // Сергей

const personTwo = appendIdentifier('Николай');
console.log(personTwo.id); // 782
console.log(personTwo.name); // Property «name» does not exist on type «Николай & { id: number; }»

Компилятор сообщает об ошибке только когда мы обращаемся к свойству name. Нужно задать ограничение — можно передавать только объекты.

const appendIdentifier = <Type extends object>(obj: Type) => {
    const id = Math.floor(Math.random() * 1000);
    return { ...obj, id };
};

const personOne = appendIdentifier({ name: 'Сергей', age: 40 });
console.log(personOne.id); // 271
console.log(personOne.name); // Сергей

const personTwo = appendIdentifier('Николай'); // Argument of type «string» is not assignable to parameter of type «object»
console.log(personTwo.id); // 782
console.log(personTwo.name); // Property «name» does not exist on type «Николай & { id: number; }»

Дженерики в интерфейсах

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

interface Collection<Type> {
    items: Type[];
    length: () => number;
}

const stringCollection: Collection<string> = {
    items: ['one', 'two'],
    length: function () {
        return this.items.length;
    },
};
console.log(stringCollection.length()); // 2

const numberCollection: Collection<number> = {
    items: [1, 2, 3],
    length: function () {
        return this.items.length;
    },
};
console.log(numberCollection.length()); // 3

Дженерики в классах

Обобщенные типы можно использовать не только с отдельными методами класса, но и с классом в целом.

class GenericClass<Type> {
    zeroValue: Type;
    add: (x: Type, y: Type) => Type;
}

let typeNumber = new GenericClass<number>();
typeNumber.zeroValue = 0;
typeNumber.add = function (x, y) {
    return x + y;
};

let typeString = new GenericClass<string>();
typeString.zeroValue = '';
typeString.add = function (x, y) {
    return x + y;
};

16. Защитники типа

Защитники типа — это правила, которые помогают механизму выведения типов более точно определить тип переменной, которая принадлежит типу объединения typeOne|typeTwo.

Первый пример

Давайте рассмотрим простой пример и создадим правило, которое поможет компилятору сузить тип переменной, объявленной как Dog|Fish — до Dog или Fish.

class Dog {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    walk() {
        console.log('Dog walk');
    }
}

class Fish {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    swim() {
        console.log('Fish swim');
    }
}

function getPet() {
    if (Math.random() < 0.5) {
        return new Dog('dog');
    }
    return new Fish('fish');
}

const pet: Dog | Fish = getPet();

pet.walk();
pet.swim();

Компилятор выдает ошибку на двух последних строчках кода. Недопустимо вызывать метод walk(), когда переменная pet имеет тип Fish. Аналогично, нельзя вызывать метод swim(), когда переменная pet имеет тип Dog. Компилятору нужна наша подсказка, чтобы сузить тип переменной pet до типа Dog или Fish. Тогда компилятору будет понятно, когда можно вызывать метод walk(), а когда — метод swim().

class Dog {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    walk() {
        console.log('Dog walk');
    }
}

class Fish {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    swim() {
        console.log('Fish swim');
    }
}

function isFish(pet: Dog | Fish): pet is Fish { // предикат типа
    return pet instanceof Fish;
}

function getPet() {
    if (Math.random() < 0.5) {
        return new Dog('dog');
    }
    return new Fish('fish');
}

const pet: Dog | Fish = getPet();

if (isFish(pet)) {
    pet.swim();
} else {
    pet.walk();
}

Если функция isFish возвращает true — компилятор будет знать, что аргумент pet имеет тип Fish. И компилятор будет знать, что если функция возвращает false — аргумент pet имеет тип Dog.

Второй пример

Сужение типа number|string до string или number с использованием ключевого слова typeof.

function doStuff(arg: number | string) {
    if (typeof arg === 'string') {
        // внутри блока компилятор знает, что arg должно быть строкой
        return arg + ' ' + arg;
    } else {
        // внутри блока компилятор знает, что arg должно быть числом
        return arg ** 2;
    }
}

Третий пример

Сужение типа Foo|Bar до Foo или Bar с использованием ключевого слова instanceof.

class Foo {
    foo = 123;
}

class Bar {
    bar = 123;
}

function doStuff(arg: Foo | Bar) {
    if (arg instanceof Foo) {
        // внутри блока компилятор знает, что arg должен быть Foo
        console.log(arg.foo); // OK
        console.log(arg.bar); // Ошибка!
    } else {
        // внутри блока компилятор знает, что arg должен быть Bar
        console.log(arg.foo); // Ошибка!
        console.log(arg.bar); // OK
    }
}

doStuff(new Foo());
doStuff(new Bar());

Четвертый пример

Сужение типа Foo|Bar до Foo или Bar с использованием ключевого слова in.

interface Foo {
    foo: number;
}
interface Bar {
    bar: string;
}

function doStuff(arg: Foo | Bar) {
    if ('foo' in arg) {
        // внутри блока компилятор знает, что arg должен быть Foo
    } else {
        // внутри блока компилятор знает, что arg должен быть Bar
    }
}

Пятый пример

Сужение типа Foo|Bar до Foo или Bar с использованием значения свойства kind.

type Foo = {
    kind: 'foo';
    foo: number;
};
type Bar = {
    kind: 'bar';
    bar: number;
};

function doStuff(arg: Foo | Bar) {
    if (arg.kind === 'foo') {
        // внутри блока компилятор знает, что arg должен быть Foo
    } else {
        // внутри блока компилятор знает, что arg должен быть Bar
    }
}

Поиск: JavaScript • TypeScript • Web-разработка • Теория • Типы данных

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.