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 interface more than once per scope (the final result will be the sum 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.
Top comments (23)
type
s are actually aliasesOne of the sudden realizations I had about types and interfaces in TypeScript is that they're not actually "types", they're type aliases. As they don't define new types, they just they give them a new name.
In other words, they could have used the keyword
alias
instead oftype
and maybe saved some troubles.This means that interfaces are "opaque" relatively to its internal structure, whereas type aliases are not. I.e., the type hint you get is just
interface IAnimal
in the former case, and the whole type alias definition in the latter.Using
new
to define constructor signaturesMy suggestion here is that you're misunderstanding the role of interface that define constructor signatures. When you do
that is not an interface you should implement. That's a type to describe the class itself. For example:
This means that if you have static properties defined on a class, you can have them defined on an interface:
On the other hand, when you're using
implements
in a class, you're describing the shape of an instance of that class. Which means you writeclass Parrot implements IAnimal {...}
, becauseParrot
instances comply to the shape defined byIAnimal
.Edit: missed one of your replies in the comments that is actually on point on that 🙂
this is interesting. I saw it somewhere before but didn't pay attention, now I see what this was about.
But there is
Record<TAnimal, string>
for this.Interesting
Function overloading seems to work similar
One more thing: as I said interface allows extension even after declaration, so it would be possible extend interface exposed by 3-rd party library, but I can't imagine good use case for it. Any ideas?
There’s an example of the usefulness of this in a older than the current version of a functional programming library fp-ts.
It’s called declaration merging and it was used in that library, that implements higher kinded types, to extend those types.
Can you please point me to the code (link to the file/commit in github)? I want to understand it better
Yes, you can have a look here:
github.com/gcanti/fp-ts/tree/1.x
Has I explained this particular feature was used extensively in V1, but I think that is no longer the case due to the way the library is now structured.
An example where you'd want to extend the interface of a 3rd party API is OrderCloud. They have the property
xp
to enable you to extend their data model. Here's an example of a type wherexp
is object (I think sometimes it'sany
too).And the extension would be something like:
From TS docs:
“As we mentioned, type aliases can act sort of like interfaces; however, there are some subtle differences.
One difference is that interfaces create a new name that is used everywhere. Type aliases don’t create a new name — for instance, error messages won’t use the alias name. In the code below, hovering over interfaced in an editor will show that it returns an Interface, but will show that aliased returns object literal type.“
This is why we prefer interfaces. It makes everything more readable.
Type syntax seems archaic and obscure, Interfaces are more direct and explicit...clarity wins
they look the same?
replace
interface
withtype
add equal sign before{
→"=" is already odd, different from classes and all other non-object "{ ... }" statements.
Also, almost no non-advanced training material I can remember talks about "type"..
Running a Dev shop, why overload people's brains with something that looks archaic and has questionable use (at least for business applications)?
Also everyone has lots of historic interface/class/type/structure luggage from other languages that doesn't map into using types in place of interfaces.
I imagine there's valid use for it for dev tool creators.
You keep using term "archaic".
a) What is your reason behind it?
b) Why archaic is a bad thing? Math notation of plus is archaic, let's change the notation?
I feel like people who have background in Java, C# would be more comfortable with notation of interface.
People who have background in functional languages, like OCaml (all ML family?) will prefer type notation.
People who don't have background - will accept whatever you show them first.
Argument
"=" is already odd
is a matter of taste. And if you don't like it based on that, there is no reason for me to argue. There are people who prefer to put semicolons in the end of JS and who don't...I would be willing to bet that despite their different capabilities (types vs interfaces), people with an OOP background will be a bit biased towards interfaces while people with FP backgrounds will tend to prefer types.
I also disagree with this, I think for one an
interface
in the OO world conveys the message that somewhere you're going to have an implementation of sorts and when you don't have at least one of those that is not being explicit.I would also argue that an
interface
conveys the message of abstraction which is not the case when one would use it to define the structure of an object which atype
does a much better of.I think a
type
is very explicit in that sense.Also, you say that
type
is archaic, well, it's not, it is used in many other languages (modern languages) to represent exactly that.I had a similar experience, which led me to open a PR for the Typescript handbook, documenting using type aliases over interfaces.
github.com/microsoft/TypeScript-Ha...
In the new section, you wrote:
But it should be:
Doesn't seem to help typescriptlang.org/play/#code/JYOw... 🤔
I guess the only reasonable usage of
new
is this:code example from stackoverflow.com/questions/134070...
If you want to declare type of constructor you can do something like this:
Yes. I wasn't unable to check it, but I was sure I did something like that. But according to the docs
typescriptlang.org/docs/handbook/i...
Seems like you are right. Constructors interfaces declarations are mostly effective as function arguments.
I don't know all the answers, I simply experimented to write this post. It also can happen I miss a lot of things, or there are bugs in TS or some small details which I miss. All ideas are more than welcome. (TypeScript documentation seems to lag behind actual behavior sometimes)
Hey there! I shared your article here t.me/theprogrammersclub and check out the group if you haven't already!