Utility Types page
Use utility types provided by TypeScript.
Overview
In this section, you will:
- Make properties optional with
Partial<Type>
. - Make properties required with
Required<Type>
. - Set properties as immutable with
Readonly<Type>
. - Remove nullability with
NonNullable<Type>
. - Map keys to a type with
Record<Keys, Type>
. - Select included subtypes with
Extract<Type, Union>
. - Exclude values from a type with
Exclude<Type, ExcludedUnion>
. - Include specific properties with
Pick<Type, Keys>
. - Exclude specific properties with
Omit<Type, Keys>
. - Get function return type with
ReturnType<Type>
.
Objective 1: Property existence modifiers
Using Partial<Type>
Converts all properties of a type to be optional. This is useful when you want to create a type that is a subset of another, with no property being strictly required.
Partial is often used when you need to partially update an object. See the example below:
enum Diet {
"Carnivore",
"Herbivore",
"Omnivore",
}
interface Dinosaur {
species: string;
diet: Diet;
age?: number;
}
type PartialDinosaur = Partial<Dinosaur>;
// PartialDinosaur is equivalent to:
interface YetPartialDinosaur {
species?: string;
diet?: Diet;
age?: number;
}
// example
function updateDinosaur(
dinosaur: Dinosaur,
fieldsToUpdate: Partial<Dinosaur>,
): Dinosaur {
return { ...dino, ...fieldsToUpdate };
}
const oldDino: Dinosaur = {
species: "Tyrannosaurus rex",
diet: Diet.Carnivore,
};
const newDino: Dinosaur = updateDinosaur(oldDino, {
diet: Diet.Omnivore,
});
In the code above, the second parameter for the function updateDinosaur
is a partial Dinosaur
.
This allows us to pass in a Dinosaur
object with one or more of the key-value pairs, without having to pass in the entire Dinosaur
object.
Using Required<Type>
Converts all optional properties of a type to required, which is the opposite of Partial
.
You might use it when you can initialize all the properties of an object and want to avoid checking for null
or undefined
for the optional properties.
enum Diet {
"Carnivore",
"Herbivore",
"Omnivore",
}
interface Dinosaur {
species: string;
diet: Diet;
age?: number;
}
type RequiredDinosaur = Required<Dinosaur>;
// RequiredDinosaur is equivalent to:
interface YetRequiredDinosaur extends Dinosaur {
age: number; // turning age property to required
}
const trex: RequiredDinosaur = {
species: "Tyrannosaurus rex",
diet: Diet.Carnivore,
age: 30,
};
if (trex.age > 30) {
// do something
}
In the code above, we are declaring trex
to type RequiredDinosaur
. This will help us skip the check if age is null step because it is a required property
Using Readonly<Type>
Makes all properties in a type readonly
, meaning that once an object is created, its properties cannot be modified.
Use it to prevent objects from being mutated.
enum Diet {
"Carnivore",
"Herbivore",
"Omnivore",
}
interface Dinosaur {
species: string;
diet: Diet;
age?: number;
}
type NamableDinosaur = { name: string } & Dinosaur; // this is an intersection between { name: string } and Dinosaur. Think { name: string } + Dinosaur
type ReadOnlyDinosaur = Readonly<NamableDinosaur>;
// Meet Bruno, read-only dinosaur
const dino: ReadOnlyDinosaur = {
name: "Bruno",
age: 27,
species: "Tyrannosaurus rex",
diet: Diet.Carnivore,
};
// Today is its birthday! Let’s attempt to increase its age:
dino.age += 1;
// Oops! TypeScript error
In the code above, we are declaring dino
to type ReadOnlyDinosaur
. This will prevent us from assigning a new value because it is a read-only object.
Using NonNullable<Type>
Excludes null
and undefined
from the union of a type, ensuring that a type only contains “non-nullable” values.
Useful to prevent any run-time errors from occurring because we forgot to assign to a property.
type Species = "Tyrannosaurus rex" | "Triceratops horridus" | null | undefined;
type NNSpecies = NonNullable<Species>;
// Could also be written as type NNSpecies = 'Tyrannosaurus rex' | 'Triceratops horridus'
In the code above, NNSpecies
will not allow null
or undefined
.
Setup 1
✏️ Create src/utilities/property-existence.ts and update it to be:
type Person = {
role: "developer";
email?: string;
id: number;
firstName: string;
lastName: string;
team: "React" | "Angular" | "backend";
};
export type UpdateablePerson = Person;
export type FullyDefinedPerson = Person;
export type NonEditablePerson = Person;
Verify 1
✏️ Create src/utilities/property-existence.test.ts and update it to be:
import {
NonEditablePerson,
UpdateablePerson,
FullyDefinedPerson,
} from "./property-existence";
import { describe, it } from "node:test";
describe("Property existence modifiers", () => {
it("partial typing works", () => {
const personToUpdate1: UpdateablePerson = {
team: "React",
};
});
it("required typing works", () => {
// @ts-expect-error
const fullyDefinedPerson: FullyDefinedPerson = {
role: "developer",
id: 5,
firstName: "string",
lastName: "string",
team: "React",
};
});
it("readonly typing works", () => {
const nonEditablePerson: NonEditablePerson = {
role: "developer",
email: "string",
id: 5,
firstName: "string",
lastName: "string",
team: "React",
};
// @ts-expect-error
nonEditablePerson.firstName = "somethingelse";
});
});
Exercise 1
Update the property-existence.ts
file so that:
UpdateablePerson
allows all properties to be optionalFullyDefinedPerson
ensures that all properties are definedNonEditablePerson
won’t allow any update to a property
Have issues with your local setup? You can use either StackBlitz or CodeSandbox to do this exercise in an online code editor.
Solution 1
If you’ve implemented the solution correctly, the tests will pass when you run npm run test
!
Click to see the solution
✏️ Update src/utilities/property-existence.ts to be:
type Person = {
role: "developer";
email?: string;
id: number;
firstName: string;
lastName: string;
team: "React" | "Angular" | "backend";
};
export type UpdateablePerson = Partial<Person>;
export type FullyDefinedPerson = Required<Person>;
export type NonEditablePerson = Readonly<Person>;
Have issues with your local setup? See the solution in StackBlitz or CodeSandbox.
Objective 2: Construct an object type
Using Record<Keys, Type>
Shortcut for defining an object where the keys are all one type and the values are all one type.
This is particularly useful when you have a type in which multiple keys share the same value Type
, so you can avoid repeating the pattern key: type;
enum Diet {
"Carnivore",
"Herbivore",
"Omnivore",
}
interface Dinosaur {
species: string;
diet: Diet;
age?: number;
}
const dinosCollection: Record<string, Dinosaur> = {
// Could also be written as Record<'trex' | 'triceratops', Dinosaur>
trex: {
species: "Tyrannosaurus rex",
diet: Diet.Carnivore,
},
triceratops: {
species: "Triceratops horridus",
diet: Diet.Herbivore,
},
};
In the code above, dinosCollection
is equivalent to:
{
[key: string]: Dinosaur
}
Setup 2
✏️ Create src/utilities/record.ts and update it to be:
export type Person =
| {
role: "developer";
email: string;
id: number;
firstName: string;
lastName: string;
team: "React" | "Angular" | "backend";
}
| {
role: "user";
email: string;
id: number;
firstName: string;
lastName: string;
isVerified: boolean;
};
export type PersonMap = unknown;
Verify 2
✏️ Create src/utilities/record.test.ts and update it to be:
import type { Person, PersonMap } from "./record";
import { describe, it } from "node:test";
describe("record tests", () => {
it("typing works", () => {
const people: Person[] = [
{
role: "developer",
email: "email@developer.com",
firstName: "Dev",
lastName: "Eloper",
team: "React",
id: 1,
},
{
role: "developer",
email: "jane@developer.com",
firstName: "Dev",
lastName: "Eloper",
team: "React",
id: 2,
},
{
role: "user",
email: "user1@developer.com",
firstName: "Great",
lastName: "User",
isVerified: false,
id: 3,
},
{
role: "user",
email: "user2@developer.com",
firstName: "Super",
lastName: "User",
isVerified: false,
id: 4,
},
];
const userMap = people.reduce((acc, person) => {
acc[person.id] = { ...person };
return acc;
}, {} as PersonMap);
});
});
Exercise 2
Update the record.ts
file to create a new object type in which the keys are the IDs of the users and the values are the User
type.
Currently, the PersonMap
type is unknown
.
Which utility type can we use here together with the Person
type to create the appropriate PersonMap
type?
Our PersonMap
should look like this:
const data: PersonMap = {
1: {
role: ...
email: ...
firstName: ...
...
}
}
Hint: Remember to use the syntax Person["id"]
to access the type of the id
property directly from the Person
interface.
Have issues with your local setup? You can use either StackBlitz or CodeSandbox to do this exercise in an online code editor.
Solution 2
If you’ve implemented the solution correctly, the tests will pass when you run npm run test
!
Click to see the solution
✏️ Update src/utilities/record.ts to be:
export type Person =
| {
role: "developer";
email: string;
id: number;
firstName: string;
lastName: string;
team: "React" | "Angular" | "backend";
}
| {
role: "user";
email: string;
id: number;
firstName: string;
lastName: string;
isVerified: boolean;
};
export type PersonMap = Record<Person["id"], Person>;
Have issues with your local setup? See the solution in StackBlitz or CodeSandbox.
Objective 3: Construct a new type by extracting from another type
Using Extract<Type, Union>
Extracts from Type
all types that are assignable to Union
.
It effectively filters out types from Type
that do not fit into Union
.
This utility is particularly useful when you want to create a type based on a subset of another type’s possibilities that meet certain criteria.
Suppose you have a union type that represents various kinds of identifiers in your application:
type ID = string | number | boolean;
type StringOrNumberID = Extract<ID, string | number>;
In the code above, StringOrNumberID
ends up being the union of string
and number
.
So why would you not simply write a union for StringOrNumberID
?
Extract
shines when used to find the intersection of two different types.
See this example:
type Adult = {
firstName: string;
lastName: string;
married: boolean;
numberOfKids?: number;
};
type Kid = {
firstName: string;
lastName: string;
interests: string[];
pottyTrained: boolean;
};
type PersonKeys = Extract<keyof Kid, keyof Adult>;
// ^? "firstName" | "lastName"
In the code above, PersonKeys
is the keys that both Kid
and Adult
have in common, which are firstName
and lastName
.
Setup 3
✏️ Create src/utilities/extract.ts and update it to be:
type Person =
| {
role: "developer";
email: string;
id: number;
firstName: string;
lastName: string;
team: "React" | "Angular" | "backend";
}
| {
role: "user";
email: string;
id: number;
firstName: string;
lastName: string;
isVerified: boolean;
};
export type Developer = unknown;
Verify 3
✏️ Create src/utilities/extract.test.ts and update it to be:
import {
Developer
} from "./extract";
import { describe, it } from "node:test";
describe("extract tests", () => {
it("typing works", () => {
const newDev: Developer = {
role: "developer",
email: "email@developer.com",
firstName: "Dev",
lastName: "Eloper",
team: "React",
id: 4,
// @ts-expect-error
isVerified: true,
};
});
});
Exercise 3
Update the extract.ts
file to use the utility type extract
on the existing Person type. Extract one of the two possible types from Person to create a new type, Developer
.
Have issues with your local setup? You can use either StackBlitz or CodeSandbox to do this exercise in an online code editor.
Solution 3
If you’ve implemented the solution correctly, the tests will pass when you run npm run test
!
Click to see the solution
✏️ Update src/utilities/extract.ts to be:
type Person =
| {
role: "developer";
email: string;
id: number;
firstName: string;
lastName: string;
team: "React" | "Angular" | "backend";
}
| {
role: "user";
email: string;
id: number;
firstName: string;
lastName: string;
isVerified: boolean;
};
export type Developer = Extract<Person, { role: "developer" }>;
Have issues with your local setup? See the solution in StackBlitz or CodeSandbox.
Objective 4: Construct a new type by excluding types from another type
Using Exclude<Type, ExcludedUnion>
Excludes from Type
all types that are assignable to ExcludedUnion
.
Useful if you want a subset of Type
.
type T1 = string | number | boolean;
type T2 = Exclude<T1, boolean>;
const value: T2 = "Hello"; // Works
In the code above, Exclude<T1, boolean>
removes boolean
from T1
, leaving string
and number
.
Setup 4
✏️ Create src/utilities/exclude.ts and update it to be:
type Person =
| {
role: "developer";
email: string;
id: number;
firstName: string;
lastName: string;
team: "React" | "Angular" | "backend";
}
| {
role: "user";
email: string;
id: number;
firstName: string;
lastName: string;
isVerified: boolean;
};
type Developer = Extract<Person, { role: "developer" }>;
export interface FrontendDeveloper {
team: string;
}
Verify 4
✏️ Create src/utilities/exclude.test.ts and update it to be:
import { describe, it } from "node:test";
import { FrontendDeveloper } from "./exclude";
describe("exclude tests", () => {
it("typing works", () => {
const brandNewDev: FrontendDeveloper = {
email: "newHire@developer.com",
team: "React",
firstName: "June",
lastName: "Jones",
id: 8,
role: "developer",
};
const incorrectDev: FrontendDeveloper = {
email: "newHire@developer.com",
// @ts-expect-error
team: "backend",
firstName: "June",
lastName: "Jones",
id: 8,
role: "developer",
};
});
});
Exercise 4
Update the exclude.ts
file to create a new type, FrontendDeveloper
that excludes the backend
value from the team property. Build on the Developer
type we previously created.
Have issues with your local setup? You can use either StackBlitz or CodeSandbox to do this exercise in an online code editor.
Solution 4
If you’ve implemented the solution correctly, the tests will pass when you run npm run test
!
Click to see the solution
✏️ Update src/utilities/exclude.ts to be:
type Person =
| {
role: "developer";
email: string;
id: number;
firstName: string;
lastName: string;
team: "React" | "Angular" | "backend";
}
| {
role: "user";
email: string;
id: number;
firstName: string;
lastName: string;
isVerified: boolean;
};
type Developer = Extract<Person, { role: "developer" }>;
export interface FrontendDeveloper extends Developer {
team: Exclude<Developer["team"], "backend">;
}
Have issues with your local setup? See the solution in StackBlitz or CodeSandbox.
Objective 5: Include and exclude specific properties
Using Pick<Type, Keys>
Creates a type by picking the set of properties Keys
from Type
.
Useful if you want a subset of Type
.
enum Diet {
"Carnivore",
"Herbivore",
"Omnivore",
}
interface Dinosaur {
species: string;
diet: Diet;
age?: number;
}
type LesserDinosaur = Pick<Dinosaur, "species" | "age">;
const lesserDino: LesserDinosaur = {
species: "Tyrannosaurus rex",
age: 27,
};
In the code above, if there is an attempt to add diet
to lesserDino
then TypeScript will throw an error.
Object literals may only specify known properties, and diet
does not exist in type LesserDinosaur
.
Using Omit<Type, Keys>
Creates a type by omitting the set of properties Keys
from Type
.
Useful if you want a subset of Type
.
enum Diet {
"Carnivore",
"Herbivore",
"Omnivore",
}
interface Dinosaur {
species: string;
diet: Diet;
age?: number;
}
type LesserDinosaur = Omit<Dinosaur, "species" | "age">;
const lesserDino: LesserDinosaur = {
diet: Diet.Carnivore,
};
lesserDino.species = "Tyrannosaurus rex";
In the code above, if there is an attempt to add species
to lesserDino
then TypeScript will throw an error.
Property species
does not exist on type LesserDinosaur
.
Both species
and age
key properties are gone!
Setup 5
✏️ Create src/utilities/include-exclude-properties.ts and update it to be:
type Person =
| {
role: "developer";
email: string;
id: number;
firstName: string;
lastName: string;
team: "React" | "Angular" | "backend";
}
| {
role: "user";
email: string;
id: number;
firstName: string;
lastName: string;
isVerified: boolean;
};
type Developer = Extract<Person, { role: "developer" }>;
interface FrontendDeveloper extends Developer {
team: Exclude<Developer["team"], "backend">;
}
export interface AdminDeveloper extends FrontendDeveloper {}
Verify 5
✏️ Create src/utilities/include-exclude-properties.test.ts and update it to be:
import { AdminDeveloper } from "./include-exclude-properties";
import { describe, it } from "node:test";
describe("include-exclude-properties tests", () => {
it("typing works", () => {
const myAdmin: AdminDeveloper = {
permissions: ["readData", "writeData"],
email: "admin@developer.com",
team: "React",
firstName: "Admin",
lastName: "Jones",
id: 8,
};
// @ts-expect-error
myAdmin.role;
});
});
Exercise 5
Update the include-exclude-properties.ts
file to expand on the implementation of FrontendDeveloper
to create a new type, AdminDeveloper
where the role
property should be replaced by a permissions
array.
Have issues with your local setup? You can use either StackBlitz or CodeSandbox to do this exercise in an online code editor.
Solution 5
If you’ve implemented the solution correctly, the tests will pass when you run npm run test
!
Click to see the solution
✏️ Update src/utilities/include-exclude-properties.ts to be:
type Person =
| {
role: "developer";
email: string;
id: number;
firstName: string;
lastName: string;
team: "React" | "Angular" | "backend";
}
| {
role: "user";
email: string;
id: number;
firstName: string;
lastName: string;
isVerified: boolean;
};
type Developer = Extract<Person, { role: "developer" }>;
interface FrontendDeveloper extends Developer {
team: Exclude<Developer["team"], "backend">;
}
export interface AdminDeveloper extends Omit<FrontendDeveloper, "role"> {
permissions: string[];
}
Have issues with your local setup? See the solution in StackBlitz or CodeSandbox.
Objective 6: Function utility types
Using ReturnType<Type>
Gets the return type of a function Type
.
enum Diet {
"Carnivore",
"Herbivore",
"Omnivore",
}
interface Dinosaur {
species: string;
diet: Diet;
age?: number;
}
declare function getDinosaur(): Dinosaur;
type D1 = ReturnType<typeof getDinosaur>;
type D2 = ReturnType<() => Dinosaur>;
In the code above, D1
and D2
are both types Dinosaur
.
Next steps
There are other built-in utility types:
Parameters<Type>
ConstructorParameters<Type>
InstanceType<Type>
ThisParameterType<Type>
OmitThisParameter<Type>
ThisType<Type>
- Intrinsic string-manipulation types:
If you would like to dive deeper into them, check the official documentation for TypeScript.