Обобщения
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)
Ссылки:
Contributors ✨
Thanks goes to these wonderful people (emoji key):
Philipp Dvinyaninov | Dmitriy Vasilev 💵 |