Skip to main content

Обобщения

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>(arg: Array<T>): string {
let result = ''
for (let i = 0; i < arg.length; i++) {
if (i > 0) result += ','
result += arg[i].toString()
}
console.log(result)
return result
}

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
}
}

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

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

interface IUser {
getInfo()
}
class User implements IUser {
_id: number
_name: string
constructor(id: number, name: string) {
this._id = id
this._name = name
}
getInfo() {
console.log('id: ' + this._id + '; name: ' + this._name)
}
}

class Employee extends User {
_company: string
constructor(id: number, name: string, company: string) {
super(id, name)
this._company = company
}

getInfo() {
console.log('id: ' + this._id + '; name: ' + this._name + '; company:' + this._company)
}
}

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

class UserInfo<T extends IUser> {
getUserInfo(user: T): void {
user.getInfo()
}
}

В методе getUserInfo мы хотим использовать функцию getInfo(), предполагая, что в качестве параметра будет передаваться объект IUser. Но чтобы нельзя было передать объекты любого типа, а только объекты IUser, устанавливается ограничения с помощью ключевого слова extends.

И затем мы можем использовать класс, передавая подходящие объекты:

let tom = new User(3, 'Tom')
let alice = new Employee(4, 'Alice', 'Microsoft')
let userStore = new UserInfo()
userStore.getUserInfo(tom)
userStore.getUserInfo(alice)

Ключевое слово 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)

Ссылки:

  1. TypeScript документация
  2. Metanit
  3. Canonium

Contributors ✨

Thanks goes to these wonderful people (emoji key):


Philipp Dvinyaninov

📖

Dmitriy Vasilev

💵

Become a Patron!