type vs interface - jellyfish-tom/TIL GitHub Wiki
[SOURCES]
- https://www.educba.com/typescript-type-vs-interface/
- https://blog.logrocket.com/types-vs-interfaces-typescript/
Only with type can we alias primitive types.
Only with type can we create union type (interface has no similar capabilities)
Declaration merging is a feature that is exclusive to interfaces. With declaration merging, we can define an interface multiple times, and the TypeScript compiler will automatically merge these definitions into a single interface definition.
An interface can extend one or multiple interfaces. Using the extendskeyword, a new interface can inherit all the properties and methods of an existing interface while also adding new properties.
For example, we can create a VIPClient interface by extending the Clientinterface:
interface VIPClient extends Client {
benefits: string[]
}
To achieve a similar result for types, we need to use an intersection operator:
type VIPClient = Client & {benefits: string[]}; // Client is a type
You can also extend an interface from a type alias with statically known members:
type Client = {
name: string;
};
interface VIPClient extends Client {
benefits: string[]
}
The exception is union types. If you try to extend an interface from a union type, you’ll receive the following error:
type Jobs = 'salary worker' | 'retired';
interface MoreJobs extends Jobs {
description: string;
}
The error thrown when a union type is not statically known
This error occurs because the union type is not statically known. The interface definition needs to be statically known at compile time.
Type aliases can extend interfaces using the intersection, as below:
interface Client {
name: string;
}
type VIPClient = Client & { benefits: string[]};
In a nutshell, both interfaces and type aliases can be extended. An interface can extend a statically known type alias, while a type alias can extend an interface using an intersection operator.
In TypeScript, the tuple type allows us to express an array with a fixed number of elements, where each element has its data type. It can be useful when you need to work with arrays of data with a fixed structure:
type TeamMember = [name: string, role: string, age: number];
Interfaces don’t have direct support for tuple types. Although we can create some workarounds like in the example below, it is not as concise or readable as using the tuple type:
interface ITeamMember extends Array<string | number>
{
0: string; 1: string; 2: number
}
const peter: ITeamMember = ['Harry', 'Dev', 24];
const Tom: ITeamMember = ['Tom', 30, 'Manager']; //Error: Type 'number' is not assignable to type 'string'.
Type aliases and interfaces are similar but have subtle differences, as shown in the previous section.
While almost all interface features are available in types or have equivalents, one exception is declaration merging. Interfaces should generally be used when declaration merging is necessary, such as extending an existing library or authoring a new one. Additionally, if you prefer the object-oriented inheritance style, using the extends keyword with an interface is often more readable than using the intersection with type aliases.
However, many of the features in types are difficult or impossible to achieve with interfaces. For example, TypeScript provides rich features like conditional types, generic types, type guards, advanced types, and more. You can use them to build a well-constrained type system to make your app strongly typed. The interface can’t achieve this.
In many cases, they can be used interchangeably depending on personal preference. But, we should use type aliases in the following use cases:
- To create a new name for a primitive type
- To define a union type, tuple type, function type, or another more complex type
- To overload functions
- To use mapped types, conditional types, type guards, or other advanced type features
Compared with interfaces, types are more expressive. Many advanced type features are unavailable in interfaces, and those features continue to grow as TypeScript evolves.
Below is an example of the advanced type feature that the interface can’t achieve.
type Client = {
name: string;
address: string;
}
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type clientType = Getters<Client>;
// type clientType = {
// getName: () => string;
// getAddress: () => string;
// }
Using mapped type, template literal types, and keyof operator, we created a type that automatically generates getter methods for any object type.
In addition, many developers prefer to use types because they match the functional programming paradigm well. The rich type expression makes it easier to achieve functional composition, immutability, and other functional programming capabilities in a type-safe manner.