TypeScript provides a lot of utilities to help us write better, more scalable, better-structured, and overall safer applications.
One of these utilities comes under the form of Utility Types, which we’ll discuss in this article.
The concept of Utility Types in TypeScript is tightly tied with that of Generics, so if you aren’t already familiar with that, I highly recommend you read this article before and then return to this one.
What Is a Utility Type?
In our context, when we talk about utility types, we talk about the Generics that TypeScript provides us with globally so that we can transform or enhance our types.
A Utility Type takes one or two generic types as arguments and gives us a transformed type based on that initial type. We’re usually annotating the arguments with the letters:
T
, which stands for Type (Or the initial type)K
, which generally stands for KeyU
, which generally stands for Union
You don’t have to use those letters, nor do you have to use letters at all!
You can use words if that makes it easier for you; however, remember that those are the standard annotations you’ll come across in most TypeScript codebases wherever you deal with Generics.
Each Utility Type comes with its own unique way of transforming our types, as well as, you’ve guessed it – different use cases.
So, with all of that said, let’s see what the most important 12 Utility Types in TypeScript are:
Readonly<T>
The Readonly
modifier allows us to make all of a type’s properties and its nested properties read-only, thus, not allowing us to change their respective values once defined or assigned.
You might say that it’s redundant since we already have the const
keyword for that.
That’s true; however, that only applies to primitive values, not objects’ properties; that’s because they are reference values.
type Person = {
firstName: string;
lastName: string;
};
const person: Readonly<Person> = {
firstName: 'Sarah',
lastName: 'McKnight',
};
person.firstName = 'Freya'; // Compilation Error
ReadonlyArray<T>
The ReadonlyArray
utility makes sure that we restrict one’s ability to mutate an original array whose type is that of an ReadonlyArray<T>
:
type MyReadonlyNumericArray = ReadonlyArray<number>;
const myArr: MyReadonlyNumericArray = [1, 2, 3];
myArr.push(4); // Will throw a compilation error
myArr.map((val) => val * 2); // Compiles, since we are not mutating the original array, but rather returning a new one
ReturnType<T>
The ReturnType
utility takes as its argument the type of a function and will give us the type of that function’s return type.
const stringifyNumber = (arg: number): string => arg.toString();
type FunctionReturnType = ReturnType<typeof stringifyNumber>; // string
We can also use the utility for methods inside classes using the ReturnType<MyClass['myFunctionName']>
syntax.
Partial<T>
The Partial
utility allows us to make a type’s properties and any nested properties optional, thus, allowing us to omit any of them when working with the transformed type.
type ContactInformation = {
facebook: string;
twitter: string;
instagram: string;
};
type Customer = {
firstName: string;
lastName: string;
contactInformation: Partial<ContactInformation>;
};
const customer: Customer = {
firstName: 'John',
lastName: 'Carter',
contactInformation: {}, // Works just fine
};
Required<T>
As opposed to Partial
, Required
makes all the properties of a generic type T
required.
type ContactInformation = {
facebook: string;
twitter: string;
instagram: string;
};
type Customer = {
firstName: string;
lastName: string;
contactInformation: Required<ContactInformation>;
};
const customer: Customer = {
firstName: 'John',
lastName: 'Carter',
contactInformation: { // Compilation Error, since there's no `instagram` value given
facebook: 'some-facebook-address',
twitter: 'some-twitter-address',
},
};
NonNullable<T>
The NonNullable
utility ensures that the value given to a parameter of a function can be neither null
, nor undefined
. This utility complements the strictNullChecks
compiler flag that’s set up in the root tsconfig.json
file.
function logger<T>(valueToLog: NonNullable<T>) {
console.log(JSON.stringify(valueToLog));
}
print('Hey');
print({ x: '2' });
print(null); // Compilation Error
print(undefined); // Compilation Error
Pick<T, K>
The Pick
utility allows us to “pick” specific keys or properties from a type; they will be separated by the |
sign.
type MyType = {
interestingKey: string;
someKey: SomeType;
someOtherKey: string;
anotherInterestingKey: string;
};
type MyInterestingType = Pick<MyType, 'interestingKey' | 'anotherInterestingKey'>;
type MyBoringType = Pick<MyType, 'someKey' | 'someOtherKey'>;
const myInterestingObject: MyInterestingType = {
interestingKey: 'interesting value',
anotherInterestingKey: 'another interesting value',
};
const myBoringObject: MyBoringType = { // Will throw a compilation error since we haven't added the `someOtherKey` property to our object
someKey: 'some value',
};
Omit<T, K>
Similarly to Pick
, the Omit
utility type expects two generics parameters:
- The type
- The property keys that wish to omit
However, the keys we’ll specify will be removed from the returned type this time.
type Person = {
firstName: string;
lastName: string;
age: number;
}
type PersonWithoutAge = Omit<Person, 'age'>;
Record<K, T>
The Record
utility allows us to create an object whose keys are K
and values are T
. It’s helpful to map a type’s properties to another type.
type GenericObject = Record<string, any>; // Will be an object whose values can be represented by any value type
type Person = Record<'firstName' | 'lastName', string>; // Equivalent to { firstName: string; lastName: string; }
The real benefit of this Generic comes when having to deal with other generic types; for example, we might have a function that transforms all the values of an object into strings:
function allValuesToString<T>(obj: T): Record<keyof T, string> {
let transfer: Partial<Record<keyof T, string>> = {};
Object.keys(obj).forEach((key) => {
transfer[key] = JSON.stringify(obj[key]);
});
return transfer as Record<keyof T, string>;
}
const car = {
make: 'Audi',
model: 'R8',
horsepower: 562
}
type Car = typeof car;
const strCar = allValuesToString(car);
Extract<T, U>
The Extract
utility takes two generic parameters:
- A type
- A Union
It constructs a type by extracting all the union members from T
that are assignable to U
:
type MyFirstUnion = "a" | "b" | "c";
type MySecondUnion = "a" | "c" | "d";
type MyType = Extract<MyFirstUnion, MySecondUnion>; // "a" | "d"
Exclude<T, U>
As opposed to Extract
, the Exclude
generic utility type will extract all of the union members of T
that are not assignable to U
:
type MyFirstUnion = "a" | "b" | "c";
type MySecondUnion = "a" | "c" | "d";
type MyType = Exclude<MyFirstUnion, MySecondUnion>; // "b"
Summary
So that’s pretty much all in terms of the major Utility Types that TypeScript provides;
Of course, there are also a handful of Generics that we’ve been left out due to the unlikeliness of you needing them in your day-to-day work; however, if you want to check them out due to personal curiosity, you can find them here.
With all of that said, I hope you’ve enjoyed the read!
If you have any feedback, be sure to leave a comment below!
Cheers!
💬 Leave a comment