TypeScript: type vs interface
Table of Contents
In one of my recent PRs I changed all interface
s to type
s because there were already more type
s than interface
s. In the review, I was asked to revert the change. I did it, but as well I wondered what the actual difference between interface
and type
. Let’s figure out this. I use the latest TS (v3.5.1) for examples in this post.
Similarities #
Records #
interface IAnimal {
name: string;
}
type Animal = {
name: string;
};
Generics #
interface IAnimal<P = string> {
name: P;
}
type Animal<P = string> = {
name: P;
};
Intersections #
type Robot = {
power: number;
};
interface IRobot {
name: string;
}
interface IRoboAnimal1 extends IAnimal, IRobot {}
interface IRoboAnimal2 extends IAnimal, Robot {}
interface IRoboAnimal3 extends Animal, IRobot {}
interface IRoboAnimal4 extends Animal, Robot {}
type RoboAnimal1 = Animal & Robot;
type RoboAnimal2 = Animal & IRobot;
type RoboAnimal3 = IAnimal & Robot;
type RoboAnimal4 = IAnimal & IRobot;
implements
#
class Dog implements IAnimal {
name: string = "good dog";
}
class Cat implements Animal {
name: string = "Where is my food, human?";
}
Extend classes #
class Control {
private state: any;
}
interface ISelectableControl extends Control {
select(): void;
}
type SelectableControl = Control & {
select: () => void;
};
Functions #
type Bark = (x: Animal) => void;
interface iBark {
(x: Animal): void;
}
and generics:
type Bark = <P = Animal>(x: P) => void;
interface iBark {
<P = Animal>(x: P): void;
}
Recursive declarations #
type Tree<P> = {
node: P;
leafs: Tree<P>[];
};
interface ITree<P> {
node: P;
leafs: ITree<P>[];
}
Exact #
type Close = { a: string };
const x: Close = { a: "a", b: "b", c: "c" };
// Type '{ a: string; b: string; c: string; }' is not assignable to type 'Close'.
interface IClose {
a: string;
}
const y: IClose = { a: "a", b: "b", c: "c" };
// Type '{ a: string; b: string; c: string; }' is not assignable to type 'IClose'.
Indexable #
type StringRecord = {
[index: string]: number;
};
interface IStringRecord {
[index: string]: number;
}
Differences #
Primitive types #
You can use only types to alias primitive types
type NewNumber = number;
interface INewNumber extends number {}
// 'number' only refers to a type, but is being used as a value here.
// this works
interface INewNumber extends Number {}
// but don't forget that 1 instanceof Number === false;
Tuples #
You can’t declare tuples with interfaces
type Tuple = [number, number];
interface ITuple {
0: number;
1: number;
}
[1, 2, 3] as Tuple; // Conversion of type '[number, number, number]' to type '[number, number]' may be a mistake
[1, 2, 3] as ITuple; // Ok
Disjoint unions #
Disjoint unions works only for types:
type DomesticAnimals = { type: "Dog" } | { type: "Cat" };
And you can’t use disjoint union types with extends
interface IDomesticAnimals extends DomesticAnimals {}
// An interface can only extend an object type or intersection of object types with statically known members
new
#
You can declare the type of new
interface IClassyAnimal {
new (name: string);
}
it doesn’t work as you expect
class Parrot implements IClassyAnimal {
name: string;
constructor(name: string) {
this.name = name;
}
}
// Class 'Parrot' incorrectly implements interface 'IClassyAnimal'.
// Type 'Parrot' provides no match for the signature 'new (name: string): void'.
constructor
doesn’t seem to work either
interface IClassyAnimal {
constructor(name: string): void;
}
class Parrot implements IClassyAnimal {
name: string;
constructor(name: string) {
this.name = name;
}
}
// Class 'Parrot' incorrectly implements interface 'IClassyAnimal'.
// Types of property 'constructor' are incompatible.
// Type 'Function' is not assignable to type '(name: string) => void'.
// Type 'Function' provides no match for the signature '(name: string): void'.
Only one declaration per scope #
You can declare types only once per scope
type Once = { a: string };
type Once = { b: string };
// Duplicate identifier 'Once'.
you can declare more than once per scope (the final result will be the join of all declarations)
interface IOnce {
a: string;
}
interface IOnce {
b: string;
}
Utility types #
Most of the time you would use types instead of interfaces to create utility types, for example:
export type NonUndefined<A> = A extends undefined ? never : A;
Conclusion #
Not all of those things were possible in early versions of TS, so people got used to interfaces. But in the latest version of TS, it seems that types are more capable and we can always use them 🤔. Or I miss something?
There are a lot of nuances in TS - something may work for a small example (which I showed), but broken for big ones. Please correct me if I missed something.
Read more: Lessons learned about code after my recent project, Server-side UI Components