Typescript: types vs interfaces
Сколько читал разных книжек и заметок - везде только общие слова и вкусовщина на тему разницы между type
и interface
в Typescript
. В лучшем случае скажут, что type
более гибкий, а interface
- он для лучшей читаемости, и классы лучше наследовать от него. Что в type
есть union
, а в интерфейсах нет, и что дженерики в общем виде не выразить через interface
…
Расскажу-ка о нетривиальных штуках, про которые почему-то не пишут:
Интерфейсы можно расширять. type
так не умеет, про это написано везде. Но как быть, если надо расширить интерфейс в другом файле? Как это выразить через import
/export
?
// a.ts
export interface Person {
name: string;
}
// b.ts
import { Person } from './a'; // ERROR: Import declaration conflicts with local declaration of 'Person'
export interface Person {
age: number;
}
Чтобы это работало, нужно в файле b.ts
изменять не сам интерфейс, а модуль:
// b.ts
export { Person } from './a';
declare module './a' {
interface Person {
age: number;
}
}
// c.ts
import { Person } from './b'; // или from './a' - типы объединены в рамках всего проекта
const person: Person = { name: 'John', age: 30 };
Тут из b.ts
можно даже не экспортировать Person
- интерфейсы уже “слиты” вместе.
В b.ts
просто нужен импорт из a.ts
хоть в каком-то виде. Сгодится и просто import "./a"
, но я делаю через экспорт, для явной иллюстрации намерений использования модифицированного интерфейса.
Это и минус интерфейсов - после такого расширения разделить интерфейс обратно и откатиться к оригинальному из a.ts
в рамках проекта станет невозможно.
Но это и плюс: интерфейсы, в отличие от type
, кешируются
…что даёт буст к производительности транслятора, поэтому рекомендуется использовать интерфейсы там, где нужны пересечения типов
Ещё одно различие: как interface
и type
работают с индексами в типах:
В интерфейсах индексы могут быть только или общими, или конкретными. Попытка описать через интерфейс перечисляемый тип - ошибка:
interface Options {
[K in 'title' | 'text']: string; // Error
// A computed property name in an interface must refer to an expression
// whose type is a literal type or a 'unique symbol' type
}
type Options = {
[K in 'title' | 'text']: string; // OK
}
Как следствие, через интерфейсы нельзя, например, выразить тип, где числовым ключам соотствует число, а строковым - строка:
interface A {
[key: string]: string;
[key: number]: number; // Error
// 'number' index type 'number' is not assignable to 'string' index type 'string'.
}
А через type
- можно:
type B = {
[key in string | number]: key extends string ? string : number;
};