Обобщения

Последнее обновление: 18.05.2021

TypeScript является строго типизированным языком, однако иногда надо построить функционал так, чтобы он мог использовать данные любых типов. В некоторых случаях мы могли бы использовать тип any:

function getId(id: any): any {
    
    return id;
}
let result = getId(5);
console.log(result);

Однако в этом случае мы не можем использовать результат функции как объект того типа, который передан в функцию. Для нас это тип any. Если бы вместо числа 5 в функцию передавался бы объект какого-нибудь класса, и нам потом надо было бы использовать этот объект, например, вызывать у него функции, то это было бы проблематично. И чтобы конкретизировать возвращаемый тип, мы можем использовать обобщения:

function getId<T>(id: T): T {
    
    return id;
}

С помощью выражения <T> мы указываем, что функция getId типизирована неким типом T. Причем на момент написания функции мы можем не знать, что именно это будет за тип. А при выполнении функции вместо Т будет подставляться конкретный тип. Причем на этапе компиляции конкретный тип не известен. И возвращать функция будет объект этого типа. Например:

function getId<T>(id: T): T {
    
    return id;
}
let result1 = getId<number>(5);
console.log(result1);
let result2 = getId<string>("abc");
console.log(result2);

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

Подобным образом еще можно использовать обобщенные массивы:

function getString<T>(args: Array<T>): string {
    
	return args.join(", ");
}

let result = getString<number>( [1, 2, 34, 5]);
console.log(result);

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

Обобщенные классы и интерфейсы

Кроме обобщенных функций и массивов также бывают обобщенные классы и интерфейсы:

class User<T> {

    private _id: T;
    constructor(id:T) {

        this._id=id;
    }
    getId(): T {

        return this._id;
    }
}

let tom = new User<number>(3);
console.log(tom.getId()); // возвращает number

let alice = new User<string>("vsf");
console.log(alice.getId()); // возвращает string

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

let tom = new User<number>(3);
console.log(tom.getId());
tom = new User<string>("vsf"); // ошибка

Все то же самое и с интерфейсами:

interface IUser<T> {

    getId(): T;
}

class User<T> implements IUser<T> {

    private _id: T;
    constructor(id:T) {

        this._id=id;
    }
    getId(): T {

        return this._id;
    }
}

Ограничения обобщений

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

function compareName<T>(obj1: T, obj2: T): void{
	
	if(obj1.name === obj2.name){
		console.log("Имена совпадают");
	}
	else{
		console.log("Имена различаются");
	}

}

Функция принимает два объекта, тип которых неизвестен. В коде функции сравниваются значения полей name этих объектов.

Попробуем с помощью этой функции сравнить два объекта, которые имеют свойство name:

let tom: {name:string} = {name: "Tom"};
let sam: {name: string} = {name: "Sam"};
compareName<{name:string}>(tom, sam);

Здесь сравниваются два объекта: tom и sam, которые имеют одини и тот же тип {name:string}. То есть оба объекта имеют свойство name.

При вызове функция compareName() типизируется этим типом - {name:string}. Казалось бы, никаких проблем не должно возникнуть. Тем не менее при компиляции мы получим ошибку:

Property 'name' does not exist on type 'T'

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

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

<T extends критерий_типов>

После названия параметра (в данном случае "T") идет ключевое слово extends, после которого указывается критерий, которому должны соответствовать типы данных, передаваемые вместо параметра "T".

Например, в случае с функцией compareName() в примере выше типы должны иметь свойство name. Поэтому перепишим функцию следующим образом:

function compareName<T extends {name:string}>(obj1: T, obj2: T): void{
	
	if(obj1.name === obj2.name){
		console.log("Имена совпадают");
	}
	else{
		console.log("Имена различаются");
	}

}

let tom: {name:string} = {name: "Tom"};
let sam: {name: string} = {name: "Sam"};
compareName<{name:string}>(tom, sam);

Запись <T extends {name:string}> означает, что параметр T должен представлять такой тип, который содержит свойство name, как в случае выше два объекта tom и sam.

Причем параметр T необязательно должен представлять именно тип {name:string}. Например:

function compareName<T extends {name:string}>(obj1: T, obj2: T): void{
	
	if(obj1.name === obj2.name){
		console.log("Имена совпадают");
	}
	else{
		console.log("Имена различаются");
	}

}

class User{ 
	constructor(public name: string, public age: number){}
}
let bob = new User("Bob", 38);
let bobic = new User("Bob", 24);
compareName<User>(bob, bobic);

type Person = {id:number; name:string};
let tom: Person = {id:1, name: "Tom"};
let sam: Person = {id: 2, name: "Sam"};
compareName<Person>(tom, sam);

Здесь в первом вызове функция compareName() типизируется классом User, то есть передаваемые в нее объекты должны представлять класс User. При втором вызове функция типизируется типом Person, который представляет объект {id:number; name:string}. Но и User и Person объединяет то, что они имеют свойство name и поэтому соответствуют ограничению {name: string}.

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

interface Named{
	name: string;
}
function compareName<T extends Named>(obj1: T, obj2: T): void{
	
	if(obj1.name === obj2.name){
		console.log("Имена совпадают");
	}
	else{
		console.log("Имена различаются");
	}
}

Подобным образом ограничения обобщений можно применять в интерфейсах и классах:

interface Named{
	name: string;
}
class NameInfo<T extends Named>{

    printName(obj: T): void{

        console.log(`Name: ${obj.name}`);
    }
}

class User{ 
	constructor(public name: string, public age: number){}
}
let bob = new User("Bob", 38);
let nameInfo1 = new NameInfo<User>();
nameInfo1.printName(bob);

type Person = {id:number; name:string}
let tom: Person = {id:1, name: "Tom"};
let nameInfo2 = new NameInfo<Person>();
nameInfo2.printName(tom);

В данном случае класс NameInfo применяет параметр типа T, который ограничивается интерфейсом Named.

И в этом случае мы можем типизировать объекты класса NameInfo любым типом, который имеет свойство name, как в данном случае классом User или типом Person.

Ключевое слово new

Чтобы создать новый объект в коде обобщений, нам надо указать, что обобщенный тип T имеет конструктор. Это означает, что вместо параметра type:T нам надо указать type: {new(): T;}. Например, следующий обобщенный интерфейс работать не будет:

function UserFactory<T>(): T {
    return new T(); // ошибка компиляции
}

Чтобы интерфейс начал работать, используем слово new:

function userFactory<T>(type: { new (): T; }): T {
    
    return new type();
}


class User {

    constructor() {
        console.log("создан объект User");
    }
}

let user : User = userFactory(User);
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850