This Crazy Syntax Lets You Get An Array Element's Type
Learn how to extract the type of an array element in TypeScript using the powerful Array[number]
trick.
You should use types by default until you need a specific feature of interfaces, like 'extends'.
Interfaces can't express unions, mapped types, or conditional types. Type aliases can express any type.
Interfaces can use extends
, types can't.
When you're working with objects that inherit from each other, use interfaces. extends
makes TypeScript's type checker run slightly faster than using &
.
Interfaces with the same name in the same scope merge their declarations, leading to unexpected bugs.
Type aliases have an implicit index signature of Record<PropertyKey, unknown>
, which comes up occasionally.
TypeScript offers a first-class primitive for defining objects that extend from other objects - an interface
.
Interfaces have been present since the very first version of TypeScript. They're inspired by object-oriented programming and allow you to use inheritance to create types:
interface WithId {
id : string;
}
interface User extends WithId {
name : string;
}
const user : User = {
id : "123",
name : "Karl",
wrongProperty : 123,Object literal may only specify known properties, and 'wrongProperty' does not exist in type 'User'.2353Object literal may only specify known properties, and 'wrongProperty' does not exist in type 'User'.};
However, they come with a built-in alternative - type aliases, declared using the type
keyword. The type
keyword can be used to represent any sort of type in TypeScript, not just object types.
Let's say we want to represent a type that is either a string or a number. We can't do that with an interface, but we can with a type:
type StringOrNumber = string | number;
const func = (arg : StringOrNumber ) => {};
func ("hello");
func (123);
func (true );Argument of type 'boolean' is not assignable to parameter of type 'StringOrNumber'.2345Argument of type 'boolean' is not assignable to parameter of type 'StringOrNumber'.
But, of course, type aliases can also be used to express objects. This leads to a lot of debate among TypeScript users. When you're declaring an object type, should you use an interface or a type alias?
If you're working with objects that inherit from each other, use interfaces. Our example above, using WithId
, can be expressed with type aliases, using an intersection type.
type WithId = {
id : string;
};
type User = WithId & {
name : string;
};
const user : User = {
id : "123",
name : "Karl",
wrongProperty : 123,Object literal may only specify known properties, and 'wrongProperty' does not exist in type 'User'.2353Object literal may only specify known properties, and 'wrongProperty' does not exist in type 'User'.};
This is perfectly fine code, but it's slightly less optimal. The reason is to do with the speed at which TypeScript can check your types.
When you create an interface using extends
, TypeScript can cache that interface by its name in an internal registry. This means that future checks against it can be made faster. With an intersection type using &
, it can't cache it via the name - it has to compute it nearly every time.
It's a small optimization, but if the interface is used many times, it adds up. This is why the TypeScript performance wiki recommends using interfaces for object inheritance - and so do I.
However, I still don't recommend you use interfaces by default. Why?
Interfaces have another feature which, if you're not prepared for it, can seem very surprising.
When two interfaces with the same name are declared in the same scope, they merge their declarations.
interface User {
name : string;
}
interface User {
id : string;
}
const user : User = {Property 'name' is missing in type '{ id: string; }' but required in type 'User'.2741Property 'name' is missing in type '{ id: string; }' but required in type 'User'. id : "123",
};
If you were to try this with types, it wouldn't work:
type User = {Duplicate identifier 'User'.2300Duplicate identifier 'User'. name : string;
};
type User = {Duplicate identifier 'User'.2300Duplicate identifier 'User'. id : string;
};
This is intended behavior and a necessary language feature. It's used to model JavaScript libraries that modify global objects, like adding methods to string
prototypes.
But if you're not prepared for this, it can lead to really confusing bugs.
If you want to avoid this, I recommend you add ESLint to your project and turn on the no-redeclare
rule.
Another difference between interfaces and types is a subtle one.
Type aliases have an implicit index signature, but interfaces don't. This means that they're assignable to types that have an index signature, but interfaces aren't. This can lead to errors like:
Index signature for type 'string' is missing in type 'x'.
// @errors: 2322
interface KnownAttributes {
x: number;
y: number;
}
const knownAttributes: KnownAttributes = {
x: 1,
y: 2,
};
type RecordType = Record<string, number>;
const oi: RecordType = knownAttributes;
The reason this errors is that an interface could later be extended. It might have a property added that doesn't match the key of string
or the value of number
.
You can fix this by adding an explicit index signature to your interface:
interface KnownAttributes {
x : number;
y : number;
[index : string]: unknown; // new!
}
Or simply, changing it to use type
instead:
type KnownAttributes = {
x: number;
y: number;
};
const knownAttributes: KnownAttributes = {
x: 1,
y: 2,
};
type RecordType = Record<string, number>;
const oi: RecordType = knownAttributes;
Isn't that strange!
type
, not interface
The TypeScript documentation has a great guide on this. They cover each feature (although not the implicit index signature), but they reach a different conclusion than me.
They recommend you choose based on personal preference, which I agree with. The difference between type
and interface
is small enough that you'll be able to use either one without many problems.
But the TS team recommends you default to using interface
and only use type
when you need to.
I'd like to recommend the opposite. The features of declaration merging and implicit index signatures are surprising enough that they should scare you off using interfaces by default.
Interfaces are still my recommendation for object inheritance, but I'd recommend you use type
by default. It's a little more flexible and a little less surprising.
Share this article with your friends
Learn how to extract the type of an array element in TypeScript using the powerful Array[number]
trick.
Learn how to publish a package to npm with a complete setup including, TypeScript, Prettier, Vitest, GitHub Actions, and versioning with Changesets.
Enums in TypeScript can be confusing, with differences between numeric and string enums causing unexpected behaviors.
Is TypeScript just a linter? No, but yes.
It's a massive ship day. We're launching a free TypeScript book, new course, giveaway, price cut, and sale.
Learn why the order you specify object properties in TypeScript matters and how it can affect type inference in your functions.