При определении функции в Typescript первый инстинкт может подсказать вам использовать существующие интерфейсы или даже классы. Это может быть не оптимально. Давайте начнем с простого примера:
interface Cat {
name: string;
furColor: string;
age: number;
}
const getName = (cat: Cat): string => {
return cat.name;
}
const cat: Cat = { name: 'Garfield', furColor: 'orange', age: 3 }
console.log(getName(cat));
В этом примере функция getName()
возвращает имя кота (Гарфилд). Пока все хорошо. Теперь предположим, что мы вводим новый интерфейс Person
:
interface Person {
name: string;
age: number;
}
const person: Person = { name: 'John', age: 30 }
console.log(getName(person));
Теперь typescript будет жаловаться на тип person
:
Argument of type 'Person' is not assignable to parameter of type 'Cat'.
Property 'furColor' is missing in type 'Person' but required in type 'Cat'.
Это имеет смысл, потому что человек — это явно не кошка (в большинстве случаев). Но это слишком ограничивает функцию, которая на самом деле использует только имя объекта, который она получает.
Сделать функцию более гибкой
Решение состоит в том, чтобы переписать функцию и сделать ее более гибкой:
const getName = (obj: { name: string }): string => {
return obj.name;
}
Теперь мы можем использовать ее для кошек, людей и любых других объектов, у которых есть свойство name
. Функции не нужно ничего другого для работы, поэтому мы не должны ограничивать ее искусственно.
Но почему это работает? Это работает потому, что Typescript только проверяет, обладает ли аргумент хотя бы свойствами, определенными в интерфейсе. Он не сравнивает фактический тип и игнорирует любые дополнительные свойства в аргументе. Это также означает, что мне пришлось построить пример таким образом, чтобы продемонстрировать свою точку зрения. Если бы я использовал Person
вместо Cat
в качестве типа параметра, компилятор принял бы и кошку. Но все же функция фактически использует только свойство name
, так почему мы должны ожидать, что параметр будет содержать возраст?
Это может показаться очевидным в данном случае. Но я видел много более сложных функций, в которых типы параметров были достаточно ограничивающими, и при этом даже не использовались все свойства функции.
Еще один шаг
Мы можем сделать еще лучше, используя дженерики.
const getName = <T extends { name: unknown }>(obj: T): T['name'] => {
return obj.name;
}
Таким образом, нам даже не нужно знать тип name
. Он будет выведен из типа объекта. Конечно, в этом особом случае нет особого смысла в том, чтобы имя было не строкой, а чем-то другим. Но это помогает подчеркнуть силу дженериков.
Заключение
Вот и все, что я хотел показать вам в этой статье. Не просто набрасывайте существующие интерфейсы на параметры функции. Подумайте о том, что на самом деле нужно функции, и требуйте только это. Это сделает ваши функции более гибкими и более удобными для повторного использования.
Дженерики — это мощный инструмент, который поможет сделать ваш код еще более многоразовым. Их, безусловно, стоит изучить еще глубже. Но это будет тема для другого раза.
Изображение: Фото 傅甬 华 на Unsplash