Every once in a while, you end up in a situation where you need some variation of a type. For instance, you might want to omit some keys, retain some keys only, or even mark all keys as undefined or required on a type, among other use cases.
Typescript offers Utility Types, which are intended to solve this particular problem. In this article, we are going to have a look at these built-in utility types and a third-party library (with examples) that offers more utilities you might find helpful in achieving the above goal.
Built-in Utility Types
This section focuses on TypeScript built-in utility types, they are numerous and I won't be able to cover all of them, I will just look at a few key ones, with examples, in my own opinions.
Partial
This utility type constructs a new type from an existing one, with the keys at the top level being marked as optional (?)
.
interface Type {
field: string;
}
type Type2 = Partial<Type>;
NB: This only runs one level, meaning keys below one level will not be affected. If you want to mark all keys as optional, regardless the level they are in, check out PartialDeep below.
Required
This utility type does the opposite of the above, constructing a new type with all keys from the old type that are optional being marked as required.
interface Type {
field?: string;
optional?: string;
}
type Type2 = Required<Type>;
Omit
This utility type constructs a new type from an existing type while omitting specified keys from the new type.
interface Type {
field1?: string;
field2: string;
field3: string;
}
type Type2 = Omit<Type, "field3" | "field1">;
Pick
This utility type constructs a new type by picking keys specified from the old type. It does the opposite of Omit, as described above.
interface Type {
field1?: string;
field2: string;
field3?: string;
field4: string;
field5?: string;
}
type Type2 = Pick<Type, "field2" | "field3">;
Readonly
This utility type constructs a new type from an existing one and marks all keys as read-only i.e. they cannot be re-assigned. This is useful for types of a frozen object - i.e. Object.freeze()
.
interface Type {
field1?: string;
field2: string;
field3: string;
}
type Type2 = Readonly<Type>;
Record
This utility type constructs a new type with union members as keys and the type as the type of the keys.
interface Name {
firstName: string;
lastName: string;
}
type Names = "user1" | "user2";
type Type2 = Record<Names, Name>;
Above are a few built-in utility types that I find very useful, you can find out more about built-in utility types in the official documentation here.
Extending Built-in Utility Types
While the above built-in utility types are amazing, they don't cover all use cases, and this is where libraries that provide more utilities fill in the gap. A good example of such a library is type-fest, which provides even more utilities.
While I won't look in to all utilities provided by type-fest, I will highlight a few that are quite help and build on the built-in types utilities.
Except
This is a variation of the Omit utility type described above, but stricter. It constructs a new type by omitting specified keys from a Type, but unlike Omit, the keys being emitted must strictly exist in the Type.
// import { Except } from "type-fest"
interface X {
a: string;
b: string;
c: string;
}
// Omit Example
type Y = Omit<X, "d">
// Except Example
type Z = Except<X, "d" >
As you can see in the image below, Except throws an error if you provide a Key that doesn't exist.
Merge
Constructs a new type by merging two Types, with keys of the second type overriding the keys of the first type.
// import { Merge } from "type-fest"
interface X {
a: string;
b: string;
c: string;
}
interface Y {
c: number;
d: number;
e: number;
}
type Z = Merge<X, Y>
const x : Z = {
a: "is string",
b: "is string",
c: 1,
d: 2,
e: 3,
}
PartialDeep
This utility type constructs a new type where all keys in all levels are optional. This is quite similar to the Partial
built-in utility type, with one significant difference, it runs deeply to all levels, while the former does it at the first level.
// import { PartialDeep } from "type-fest";
interface X {
a: string;
b: string;
c: string;
}
interface Y {
c: number;
d: number;
e: number;
f: X;
}
type Z = PartialDeep<Y>;
const x: Z = {};
ReadonlyDeep
This utility type constructs a new type with all keys on all levels marked as required. This is also similar to the built-in Readonly
utility type, but unlike the built-in utility type, this one goes down to all keys in all levels, making them immutable.
Mutable
This utility type constructs a type that strips out readonly
from a keys in a type, essentially the opposite of what the built-in utility type Readonly
does.
// import { Mutable } from "type-fest";
interface X {
readonly a: string;
readonly d: string;
}
type Y = Mutable<X>;
Conclusion
In this article, I looked into typescript utility types and how they can help you automatically create types from existing ones without resulting to duplicating eliminating the need to keep related types in sync.
I highlighted a few built-in utility types that I find particularly useful on my day to day job as a developer. On top of that, we looked into type-fest, a library with a lot of utility types that extends the built-in types, and highlighted just a few.
Top comments (3)
I dunno, maybe just use normal JS where all this is 10x easier??
That's like saying just use Python without type hints because it's easier that way. What we should strive for as developers is writing maintainable software and TypeScript helps with this immensely (compared to JS). The reason why TypeScript even came to existence is because teams working on the large JS code bases in Microsoft have realized this.
Fair enough. Seems to me like a crutch for developers coming from strictly typed languages, who don't want to do things a different way - but, each to their own I guess. I actually prefer Python without type hints too :)