TypeScript. Начало работы, часть 5 из 7
03.04.2023
Теги: JavaScript • TypeScript • Web-разработка • Теория • ТипыДанных
Основы 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-разработка • Теория • Типы данных