Looking for a specific post? Checkout the Blog Index.

By Kimserey Lam with

Interface And Type In Typescript

Jul 26th, 2019 - written by Kimserey with .

One thing that I had a hard time to understand when starting to work with Typescript was the differences between Type and Interface. Even after working with TS for quite some time it can be difficult to see the benefits and interesting combinations one can provide over the other. Tslint is very helpful in that regards with a set of rules enforcing the use of the right definition. So today we will look in more details on the differences and the benefits of Type and Interface.

  1. Linter Rules
  2. Union And Intersection
  3. Declaration Merging

Linter Rules

Tslint has few properties which provide nice hints on how and where different instantiations are preferred.

The first linter rule is no-empty-interface which as the name says, prevents us from defining empty interface.

1
2
// An empty interface is equivalent to `{}`. (no-empty-interface)tslint(1)
interface Person {}

The second rule is interface over type literal which states that interfaces should be preferred over type literals.

1
2
3
4
5
// Use an interface instead of a type literal. (interface-over-type-literal)tslint(1)
type Person = {
    firstname: string,
    lastname: string
};

So instead we would use an interface:

1
2
3
4
interface Person {
    firstname: string;
    lastname: string;
}

Notice the difference in syntax where a type literal is assigned a type versus how the interface is declared. The last rule is callable-types which states that callable types should be specified as type literals.

1
2
3
4
// Interface has only a call signature — use `type SayHi = () => void;` instead. (callable-types)tslint(1)
interface SayHi {
    (): void;
}

For a single call signature, as specified by tslint, we can alias it:

1
type SayHi = () => void;

This is also known as Type alias. In contrast, a function signature would be defined in an interface:

1
2
3
interface CanSayHi {
    sayHi(): void;
}

Now that we have a hint of what is preferred by tslint, we can look into more fun features available.

Union And Intersection

Intersection is defined with &. It creates a new type literal containing the combination of all properties from the types intersected.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Person {
    firstname: string;
    lastname: string;
}

interface Profile {
    email: string;
}

type User = Person & Profile;

const user: User = {
    firstname: "Kimserey",
    lastname: "Lam",
    email: "[email protected]"
};

Union is defined with | and creates a new type literal which can be either one of the type provided or all at the same time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type UserIdentifier = Person | Profile;

// UserIdentifer is Person
const id1: UserIdentifier = {
    firstname: "Kimserey",
    lastname: "Lam"
};

// UserIdentifier is Profile
const id2: UserIdentifier = {
    email: "[email protected]"
};

// UserIdentifier is Person & Profile
const id3: UserIdentifier = {
    firstname: "Kimserey",
    lastname: "Lam",
    email: "[email protected]"
};

Union is common in function parameter where argument type can be different values:

1
2
3
function example(value: string | number) {
    // ...
}

Object literals can also be used to directly unite the types without having to explicitly name the type.

1
2
3
4
5
6
7
8
interface Person {
    firstname: string;
    lastname: string;
}

type User = Person & {
    email: string;
};

Using the union and the flow refinement, we can construct Discriminated Unions.

1
2
3
4
5
6
7
8
9
10
11
interface PostMessage {
    type: "Post";
    body: string;
}

interface ReceiveMessage {
    type: "Receive";
    body: string;
}

type Action = PostMessage | ReceiveMessage;

Action is the union between PostMessage and ReceiveMessage. Because we have restricted the type to a string literal, we can leverage TypeScript flow refinement with a switch case:

1
2
3
4
5
6
7
8
9
10
function doSomething(action: Action) {
    switch (action.type) {
        case "Post":
            console.log(action.body);
            break;
        case "Receive":
            console.log(action.author, action.body);
            break;
    }
}

Flow refinement allows the compiler to restrict the type as the flow of the code breaks into branches. On the first case, the compiler will know that the type of the action is PostMessage and therefore that .author is not available.

Declaration Merging

Declaration merging is a special a feature of TypeScript allowing to spread the declaration of the shape of an object within multiple interfaces.

Taking back our previous example, we can declare twice Person:

1
2
3
4
5
6
7
8
interface Person {
    firstname: string;
    lastname: string;
}

interface Person {
    email: string;
}

The resulting interface is the union of all members. Functions of the same name are treated as overload.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface CanSayHi {
    sayHi(text: string): void;
}

interface CanSayHi {
    sayHi(text: string, person: string): void;
}

class MyClass implements CanSayHi {
    public sayHi(text: string): void;
    public sayHi(text: string, person: string): void;
    public sayHi(text: any, person?: any) {
        throw new Error("Method not implemented.");
    }
}

Declaration merging can also be used to extend existing interfaces as I described in my previous blog post on how to extend existing libraries. This is achieve using declaration merging by declaring an interface augmentation on a specific module:

1
2
3
4
5
6
7
import { A } from "./my-module";
declare module "./my-module" {
    interface A {
        saySomething(): void;
    }
}
A.prototype.saySomething = () => {};

The declaration in itself is a only a hint to let the compiler know that we have extended A coming from my-module with a new function else it wouldn’t be able to know that we have extended it via its prototype.

Conclusion

Today we explored the slight differences between types and interfaces by looking at the recommendations from tslint. We then looked into some nice features from the compiler allowing us to union or intersect types and how it enabled the use of discriminated unions. Lastly we completed the post by looking at declaration merging which is a special feature from TypeScript. I hope you like this post and I see you on the next one!

External Resources

Designed, built and maintained by Kimserey Lam.