TypeScript Interface vs Type Aliases



В TypeScript для описания структуры объекта можно использовать как интерфейсы, так и типы. В статье разберу схожие черты типа и интерфейса, их отличия, дам рекомендации когда и что использовать.
Сходства между типом и интерфейсом
Допустим, у нас есть объект "пользователь" с именем и возрастом. Можем описать его как с использованием интерфейса, так и типа:
type TUser = {
name: string;
age: number;
}
interface IUser {
name: string;
age: number;
}
Одинаковые ошибки
В случае, если мы создадим неправильный объект с этим типом или интерфейсом, в обоих случаях получим ошибку:
const user1: IUser = {
name: 'Mark',
age: 18,
country: 'Russia' // указали лишнее поле
}
/*
Type '{ name: string; age: number; country: string; }' is not assignable to type 'IUser'. Object literal may only specify known properties, and 'country' does not exist in type 'IUser'.
*/
// Точно такую же ошибку получим, если укажем user1: TUser
Можем использовать сигнатуру индекса
type TDict = {
[key: string]: string
};
interface IDict {
[key: string]: string;
}
const myName: IDict = {
name: "Peter"
}
Можем определить тип функции
// тип чуть лаконичнее
type TFn = (x: number) => string;
interface IFn {
(x: number): string;
}
const toStrT: TFn = x => '' + x;
const toStrI: IFn = x => '' + x;
В обоих случаях можем описать функцию со свойствами:
// в данном случае тип и интерфес практически идентичны
type TSquareFn = {
(x: number): number;
prop: string;
}
interface ISquareFn {
(x: number): number;
prop: string;
}
const squareNum: ISquareFn = (x: number) => x * x;
// Если не укажем свойство ниже, получим ошибку Property 'prop' is missing in type '(x: number) => number' but required in type 'TSquareFn'.
squareNum.prop = 'This is a property';
Могут быть обобщенными
type TPair<T> = {
first: T;
second: T;
}
interface IPair<T> {
first: T;
second: T;
}
const myFirstPair: TPair<number> = {
first: 1,
second: 2
}
const mySecondPair: IPair<string> = {
first: '1',
second: '2'
}
Расширяемы и взаиморасширяемы
На основе типа и интерфейса можно создавать расширенные типы:
interface IPartialPointX { x: number; }
interface IPoint extends IPartialPointX { y: number; }
const pointXI: IPartialPointX = {
x: 1920
}
const pointI: IPoint = {
x: 1920,
y: 1080
}
type TPointX = {x:number};
type TPoint = TPointX & {y:number};
const pointXT: TPointX = {
x: 1920
}
const pointT: IPoint = {
x: 1920,
y: 1080
}
Более того, тип и интерфейс взаимо-расширяемы:
// Расширенный интерфейс на основе типа
type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }
// Расширенный тип на основе интерфейса
interface PartialPointX { x: number; };
type Point = PartialPointX & { y: number; }
Может реализовывать класс
interface IPoint {
x: number;
y: number;
}
class SomePoint implements Point {
x = 1;
y = 2;
}
type TPoint = {
x: number;
y: number;
};
class SomePoint2 implements TPoint {
x = 1;
y = 2;
}
Пример с конструктором и методом:
interface UserInterface {
name: string;
age: number;
sayHello: () => string;
}
class User implements UserInterface{
constructor(public name: string, public age: number) {}
sayHello() {
return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
}
}
const user: UserInterface = new User('John', 30);
console.log(user.sayHello()); // "Hello, my name is John and I am 30 years old."
Разница между типом и интерфейсом
Тип более универсальный подходит не только для объектов
// primitive
type Name = string;
const userName: Name = 'Peter';
// union
type StrOrNum = number | string;
const n1: StrOrNum = 1;
const n2: StrOrNum = '2';
// tuple
type Data = [number, boolean, string];
const arr: Data = [1, false, 'Hey'];
// arrays
type NameList = string[];
const names: NameList = ['yana', 'sveta', 'bogdan']
type NamedNums = [string, ...number[]];
const nums: NamedNums = ['ages', 15, 25, 18, 49]
В интерфейсах мы можем использовать tuple, но это менее удобно и ограничивает использование встроенных методов массивов
type TData = [number, boolean, string];
const data1:TData = [1, true, 'hello'];
const data2:TData = [2, false, 'world'];
const data3 = data1.concat(data2); // [1, true, 'hello', 2, false, 'world']
interface IData { 0: number; 1: boolean; 2: string; length: 3;}
const iData1:IData = [1, true, 'hello'];
const iData2:IData = [2, false, 'world'];
const iData3 = iData1.concat(iData2); // Property 'concat' does not exist on type 'IData'
Расширение типа из union
Рассмотрим такой пример:
type Input = { msg: string };
type Output = { success: boolean };
interface VariableMap {
[name: string]: Input | Output;
}
const yourVar: VariableMap = {
data: { msg: 'input text' }
}
const myVar: VariableMap = {
response: { success: true }
}
В этом примере мы создали интерфейс объекта с динамическим ключом и значением, соответствующим Input или Output. Но что если мы хотим сделать объект, первое свойство которого будет соответствовать Input или Output, а второе — то, которое мы добавим? С интерфейсом такое сделать не получится, а псевдоним типа поможет нам решить эту задачу:
type NamedVariable = (Input | Output) & { name: string };
const otherVar: NamedVariable = {
msg: 'test',
name: 'peter'
}
const anotherVar: NamedVariable = {
success: true,
name: 'angela'
}
interface Test extends (Input | Output) { name: string };
// Выдаст ошибку: An interface can only extend an identifier/qualified-name with optional type arguments.
Для интерфейса доступно объединение деклараций
Вернемся к первоначальному примеру:
interface IUser {
name: string;
age: number;
}
// объединяем декларации
interface IUser {
country: stringl
}
const user1: IUser = {
name: 'Mark',
age: 18,
country: 'Russia' // устранили ошибку
}
Когда выбрать тип, а когда интерфейс
Псевдоним типа (Type Alias) следует использовать в следующих случаях:
- Для примитивов
- Для массивов и кортежей (tuples), особенно если впоследствии хотим использовать методы массивов
- Для гибкого расширения типа с использованием union
- Для типов внутри проекта, не связанных с API
Интерфейс, как правило, используется для:
- Типизации объектов
- Для декларации типов API, благодаря возможности объединения деклараций, что может быть полезно при изменении API
Стоит также учитывать согласованность — если в проекте установлен единый стиль, лучше придерживаться этого стиля. Если на вашем проекте для описания пропсов, а не тип компонентов React используются интерфейсы, а не типы — при создании нового компонента будет логично создать интерфейс пропсов.
Итого
- Интерфейс и тип можно использовать для типизации объектов.
- При работе с объектами интерфейс предпочтителен для описания данных API, а псевдоним типа — для внутренних частей кода (например, входные параметры React)
- Псевдоним типа позволяет описывать примитивы, а интерфейс — только объекты
- Псевдоним типа более гибкий — примитивы, массивы, union, tuples.
- Стоит быть консистентным и придерживаться стиля проекта.