Today, more and more people and companies are using TypeScript. It is among the top 10 programming languages in the PYPL index. And this is not surprising, since TypeScript allows you to catch many errors at compile time, not at runtime, that is before they are released to production.
Top 10 programming languages according to PYPL Index as of February 2023.
Some of you who decide to try TypeScript might get confused with type aliases and interfaces because they are both similar and used to define the shape and structure of values in a statically typed way. This means that it is possible to describe the same object using a type alias or an interface:
// Option #1: using a type alias
type Person = {
firstName: string;
lastName: string;
age: number;
};
// Option #2: using an interface
interface Person {
firstName: string;
lastName: string,
age: number;
};
You might be asking yourself why we have two options for describing the Person
object instead of one. And are there any differences or specific use cases between type aliases and interfaces? Yes, and you will find out about them in a moment.
Before we begin, I should mention that the correct term for a type is type alias, but for simplicity, I will use those two terms interchangeably.
Difference #1: Objects and Functions
Both types and interfaces can be used to describe the shape of an object or a function signature. However, the syntax is different. A type is defined using the type
keyword, followed by the name, an equals sign and the type definition:
type SampleObject = {
a: string;
b: string;
};
An interface is defined using the interface
keyword, followed by the name and the interface definition in curly braces:
interface SampleObject {
a: string;
b: string;
};
As you can see, there is not much difference in syntax between the two examples. However, when we want to define a function signature, the difference becomes more noticeable:
// Using the "type" keyword
type SampleFunction = (param1: string, param2: number) => boolean;
// Using the "interface" keyword
interface SampleFunction {
(param1: string, param2: number): boolean;
}
In practice, either approach can be used to define a function in TypeScript, and the choice between them often comes down to personal preference or project standards.
Difference #2: Other Types
Unlike an interface, the type alias can be used to create named types for all other types including:
primitives
unions (union types)
tuples
mapped types
Primitives
Just in case, I will remind you that there are six primitive types in JavaScript and TypeScript: boolean, number, string, null, undefined, and symbol. Here is an example of how to represent primitives in TypeScript using the type
keyword:
type Name = string;
type Age = number;
type IsStudent = boolean;
With interfaces, you cannot directly define a named type for a primitive type.
Unions
A variable with a union type can hold a value of any of the constituent types. Union types can be formed by combining two or more other types, separated by a vertical bar (|
).
// Example #1: union type that allows a variable to hold
// one of the three specified string literal values.
type SampleType1 = 'Coffee' | 'Tea' | 'Water';
// Example #2: union type that allows a variable to hold
// a value that is either a string or a number.
type SampleType2 = string | number;
// Example #3: union type that allows a variable to hold
// any of the values that are allowed by SampleType1 or SampleType2
type SampleType = SampleType1 | SampleType2;
Same as with primitives, you cannot directly define a union type using the interface
keyword, however, it is possible to have a union type as an object property:
interface SampleObject {
myProperty: string | number;
}
There are some limitations when using union types that you should be aware of:
- An interface cannot extend a union type:
type SampleUnionType = string | number;
// The code below will not work
interface extends SampleUnionType { ... }
- A class cannot implement a union type:
type SampleUnionType = string | number;
// The code below will not work
class SampleClass implements SampleUnionType { ... }
A class can implement an interface or type alias, both in the same way. However, it can not implement a type alias that names a union type.
Tuples
A tuple is a type that allows you to specify a fixed-length array of values where each element has a specific data type. Here is an example of how to create a type for an array with only two elements:
type PersonTuple = [string, number];
const person: PersonTuple = ["John Doe", 25];
Even though this can be achieved using interfaces as shown below, you will lose access to all of the array methods. If you try to call person.length
on the variable created from the interface, you will get the following error: Property 'length' does not exist on type 'PersonTuple'.
You will have to manually add those methods (length, push, concat, etc.) to the interface as shown below. However, this is not something you want to do. If you need a tuple, consider using a type alias.
// How to mimic array methods
interface PersonTuple
{
0: string;
1: number;
length: 2;
}
const person: PersonTuple = ["John Doe", 25];
console.log(person.length); // This will now work
Mapped Types
Mapped types allow you to transform an existing type into a new type by applying a mapping operation to each property of the original type.
type NewType = {
[Property in OldType]: MappingOperation;
}
Here, OldType
is the original type that you want to transform, and Property
represents each property in OldType
. MappingOperation
is a TypeScript type operator that defines how each property in OldType
will be transformed.
In the example below we create a new type B
, which has all the properties of A
, but with each property made optional.
type B = {
[Property in keyof A]?: A[Property];
}
TypeScript does not currently support typing mapped types with interfaces.
Difference #3: Declaration Merging
If you have two interfaces that you define with the same name in the same scope, they will be merged. Consider this example:
// Declaring SampleType interface for the 1st time
interface SampleType {
a: number;
b: number;
};
// Declaring SampleType interface for the 2nd time
interface SampleType {
c: number;
d: number;
};
// The second interface declaration extends the first one by adding
// two new properties. Now SampleType has four properties: a, b, c, d
const sampleType: SampleType = {
a: 1,
b: 2,
c: 3,
d: 4,
};
We cannot do the same with types. This is important to consider if you are the author of a library and want your users to be able to extend its functionality by merging interfaces. It will allow them to combine their own type declarations with those provided by the library, effectively "extending" or "augmenting" the library's API.
Difference #4: Extention
Interfaces can extend other interfaces using the extends
keyword which allows for greater flexibility and modularity:
interface Named {
name: string;
}
interface Ageable {
age: number;
}
interface Person extends Named, Ageable {
sayHello(): void;
}
Types can also be extended using an intersection type. An intersection type combines multiple types into one. Consider this example:
// Example #1
type A = { a: number };
type B = { b: number };
type C = A & B;
const c: C = { a: 10, b: 20 };
// Example #2
type A = { a: number };
type B = A & { b: number };
const b: B = { a: 10, b: 20 };
As you can see, the &
operator is used to create a new type by combining multiple existing types. The new type has all properties of the existing types.
We can also mix them together as shown below. Interfaces and type aliases are not mutually exclusive. An interface can extend a type alias and vice versa.
- Interface extends another interface:
interface A { sampleProperty: number; }
interface B extends A { sampleProperty: number; }
- Interface extends a type alias:
type A = { sampleProperty: number; }
interface B extends A { sampleProperty: number; }
- Type alias extends another type alias:
type A = { sampleProperty: number; };
type B = A & { sampleProperty: number; };
- Type alias extends interface:
interface A { sampleProperty: number; }
type B = A & { sampleProperty: number; };
There are some limitations to what can be extended with interfaces. For example, primitive types, union types, classes, and enums, cannot be extended with interfaces.
What Should I Use?
In general, it is recommended to use interfaces for defining object shapes, and types for defining other types of values. However, it is up to you and your team to decide which option to use as long as you are consistent.
For example, every object should be typed as an interface and everything else as a type. Or you can use interfaces or types for everything, but not both for values of the same type (e.g. types and interfaces for objects). Communication with your team and consistency are crucial for quality code.
Here is a good cheat sheet that I often refer to, however, it may differ for everyone:
When to use type
:
Use
type
when defining primitivesUse
type
when defining unionsUse
type
when defining tuplesUse
type
when defining functionsUse
type
when defining mapped types
When to use interface
:
Use
interface
for all object types where usingtype
is not requiredUse
interface
when you want to take advantage of declaration merging
Keep an Eye on the Changelog
The limitations and differences described in this article are current as of February 2023. However, with new versions of TypeScript, they can change. It is important to keep an eye on the release notes to stay informed about new changes and features.
The end. I hope you found this information helpful, stay tuned for more content! :)