DevSurge 💦

TypeScript Interface vs Type Aliases

Cover Image for TypeScript Interface vs Type Aliases
Mark Nelyubin
Mark Nelyubin

В 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 используются интерфейсы, а не типы — при создании нового компонента будет логично создать интерфейс пропсов.

Итого

  1. Интерфейс и тип можно использовать для типизации объектов.
  2. При работе с объектами интерфейс предпочтителен для описания данных API, а псевдоним типа — для внутренних частей кода (например, входные параметры React)
  3. Псевдоним типа позволяет описывать примитивы, а интерфейс — только объекты
  4. Псевдоним типа более гибкий — примитивы, массивы, union, tuples.
  5. Стоит быть консистентным и придерживаться стиля проекта.

Другие материалы

Cover Image for TypeScript Utility Types — вспомогательные типы и области их применения

TypeScript Utility Types — вспомогательные типы и области их применения

что такое Utility Types в TypeScript, расскажу про основные вспомогательные типы, и покажу, как применять их на реальных проектах.

Mark Nelyubin
Mark Nelyubin
Cover Image for Принципы SOLID с примерами на JS и Vue

Принципы SOLID с примерами на JS и Vue

Расскажу про принципы SOLID с актуальными примерами на JavaScript, Vue, React.

Mark Nelyubin
Mark Nelyubin