28
头图

Interfaces and type aliases are very similar, and in most cases the two can be interchanged. When writing TS, everyone must have asked themselves this question, which one should I use? Hope that after reading this article, I will give you an answer. To know when to use which one, you should first understand the similarities and differences between the two, and then make a choice.

Similarities in interface vs type alias

1. Both can be used to describe objects or functions

interface Point {
  x: number
  y: number
}

interface SetPoint {
  (x: number, y: number): void;
}
type Point = {
  x: number;
  y: number;
};

type SetPoint = (x: number, y: number) => void;

2. Both can be extended

The expansion methods of the two are different, but they are not mutually exclusive. Interfaces can extend type aliases, and similarly, type aliases can also extend interfaces.

The extension of the interface is inheritance, which is realized extends The extension of the type alias is the cross type, which is implemented &

// 接口扩展接口
interface PointX {
    x: number
}

interface Point extends PointX {
    y: number
}
// 类型别名扩展类型别名
type PointX = {
    x: number
}

type Point = PointX & {
    y: number
}
// 接口扩展类型别名
type PointX = {
    x: number
}
interface Point extends PointX {
    y: number
}
// 类型别名扩展接口
interface PointX {
    x: number
}
type Point = PointX & {
    y: number
}

Differences between interface vs type alias

1. Type aliases are more general (interfaces can only declare objects and cannot rename basic types)

The right side of a type alias can be any type, including basic types, primitive ancestors, and type expressions ( & or | ); and in the interface declaration, the right side must be a structure. For example, the following type aliases cannot be converted to interfaces:

type A = number
type B = A | string

2. Behave differently when expanding

When expanding an interface, TS will check whether the expanded interface can be assigned to the expanded interface. Examples are as follows:

interface A {
    good(x: number): string,
    bad(x: number): string
}
interface B extends A {
    good(x: string | number) : string,
    bad(x: number): number // Interface 'B' incorrectly extends interface 'A'.
                           // Types of property 'bad' are incompatible.
                           // Type '(x: number) => number' is not assignable to type '(x: number) => string'.
                           // Type 'number' is not assignable to type 'string'.
}

However, this situation does not occur when the intersection type is used. We rewrite the interface in the above code into type aliases and replace extends with the transaction set operator & . TS will do its best to combine the extended and extended types without throwing a compile-time error.

type A = {
    good(x: number): string,
    bad(x: number): string
}
type B = A & {
     good(x: string | number) : string,
     bad(x: number): number 
}

3. Behave differently when defined multiple times

Interfaces can be defined multiple times, and multiple declarations will be merged. However, if the type alias is defined multiple times, an error will be reported.

interface Point {
    x: number
}
interface Point {
    y: number
}
const point: Point = {x:1} // Property 'y' is missing in type '{ x: number; }' but required in type 'Point'.

const point: Point = {x:1, y:1} // 正确
type Point = {
    x: number // Duplicate identifier 'A'.
}

type Point = {
    y: number // Duplicate identifier 'A'.
}

Which one should I use

If both interfaces and type aliases can be satisfied, which one should be used is our concern. It feels like either, but it is strongly recommended that you use the interface first as long as it can be implemented with the interface, and use the type alias if the interface can't meet the requirements.

Why is it so recommended? In fact, it is explained in the TS wiki. The specific article address is here .

The following is the translation Preferring Interfaces Over Intersections

Most of the time, for declaring an object, type aliases and interfaces behave similarly.

interface Foo { prop: string }

type Bar = { prop: string };

However, when you need to implement other types by combining two or more types, you can choose to use interfaces to extend the types, or you can use cross types (types & ) to complete. This is the beginning of the two Time to make a difference.

  • The interface will create a single flat object type to detect attribute conflicts. It will prompt when there is an attribute conflict, while the cross type is only recursively merged attributes. In some cases, the type never
  • The interface usually performs better, and when the cross type is a part of other cross types, it is not intuitively displayed, and it is still considered as a combination of different basic types
  • The inheritance relationship between interfaces will be cached, and the cross type will be seen as a combined whole
  • When checking a target cross type, each component will be checked before the target type is checked

The above-mentioned differences are somewhat convoluted from the literal understanding, and the following will illustrate them through specific examples.

interface Point1 {
    x: number
}

interface Point extends Point1 {
    x: string // Interface 'Point' incorrectly extends interface 'Point1'.
              // Types of property 'x' are incompatible.
              // Type 'string' is not assignable to type 'number'.
}
type Point1 = {
    x: number
}

type Point2 = {
    x: string
}

type Point = Point1 & Point2 // 这时的Point是一个'number & string'类型,也就是never

It can be seen from the above code that if the interface inherits the attribute of the same name does not satisfy the definition, an error will be reported, and the intersection type is a simple merging, and finally the number & string type is produced, which can explain the first difference in the translation. In fact, we are in a different point module. The extensions introduced behave differently.

Let's look at the following example:

interface PointX {
    x: number
}

interface PointY {
    y: number
}

interface PointZ {
    z: number
}

interface PointXY extends PointX, PointY {
}

interface Point extends PointXY, PointZ {
   
}
const point: Point = {x: 1, y: 1} // Property 'z' is missing in type '{ x: number; y: number; }' but required in type 'Point'
type PointX = {
    x: number
}

type PointY = {
    y: number
}

type PointZ = {
    z: number
}

type PointXY = PointX & PointY

type Point = PointXY & PointZ

const point: Point = {x: 1, y: 1} // Type '{ x: number; y: number; }' is not assignable to type 'Point'.
                                  // Property 'z' is missing in type '{ x: number; y: number; }' but required in type 'Point3'.

It can be seen from the error report that when the interface is used, the error report will be accurately located to the Point.
But when using the cross type, although our Point cross type is PointXY & PointZ , when the error is reported, the positioning is not in Point , but in Point3 , even though our Point type does not directly reference the Point3 type.

If we put the mouse on the cross type Point type, the prompt is also type Point = PointX & PointY & PointZ instead of PointXY & PointZ .

This example can explain the second and last differences in the translation at the same time.

in conclusion

Some students may ask, if I don't need to combine and just define the type, can I just use it casually. However, for the scalability of the code, it is recommended to use the interface first. No need now, who knows if it is needed in the future? So, let us use the interface boldly~


阳呀呀
2.2k 声望2.7k 粉丝