JavaScript and TypeScript course Β· Module 6: TypeScript Basics

TypeScript Utility Types

7 min read
In this lesson10

Dr. Henry Wu reviews the genetic database of Jurassic Park. "We have a Dinosaur interface with 15 properties," he says, "but for the update form I need only 5, and for the preview just 3. Do I have to create a separate interface for each case?" Copying interfaces has a hidden cost: when Dinosaur gets a new field, every copy has to be fixed by hand, and someone always forgets one. Fortunately, TypeScript offers Utility Types, ready-made transformations that create new types based on existing ones.

What are utility types?

Utility types are generic types built into TypeScript that transform existing types into new ones. You pass the type to transform in angle brackets, like the argument of the generic functions from earlier lessons. You do not need to import them, and they disappear after compilation, because they exist only while types are being checked. Instead of separate interfaces, you have one base type from which you derive variants, like breeding lines grown from a single DNA sample.

Partial<T> - all properties optional

In the examples we will use a simplified interface with five fields. The dietType field accepts one of three values written as a union of literals. Partial<T> makes all properties of type T optional. It is the ideal type for an update, where we do not want to require every field:

1interface Dinosaur {
2  name: string;
3  species: string;
4  dangerLevel: number;
5  enclosureId: string;
6  dietType: 'herbivore' | 'carnivore' | 'omnivore';
7}
8
9// Partial<Dinosaur> = all fields optional
10type DinosaurUpdate = Partial<Dinosaur>;
11
12// Now we can pass only the changed fields
13function updateDinosaur(id: string, changes: DinosaurUpdate): void {
14  console.log(`Updating dinosaur ${id}:`, changes);
15}
16
17// Correct - we pass only what we are changing
18updateDinosaur('DINO-001', { dangerLevel: 9 });
19updateDinosaur('DINO-002', { name: 'Blue', enclosureId: 'E-7' });

The Dinosaur interface itself did not change, because Partial creates a new type next to it. Checking still works: a typo such as dangerLvl gets reported. Partial is shallow, so the fields of a nested object remain required.

Required<T> - all properties required

Required<T> works the opposite way to Partial: it removes the question mark, so all properties, even optional ones, become required:

1interface DinosaurConfig {
2  name: string;
3  trackingChip?: boolean;  // optional
4  feedingSchedule?: string; // optional
5}
6
7// Required<DinosaurConfig> = everything required
8type CompleteDinosaurConfig = Required<DinosaurConfig>;
9
10// Now trackingChip and feedingSchedule are also required
11const config: CompleteDinosaurConfig = {
12  name: 'Rex',
13  trackingChip: true,        // must be provided
14  feedingSchedule: '08:00'   // must be provided
15};

If feedingSchedule were missing, the compiler would report the missing property. This is useful when a configuration must be complete after the default values have been filled in.

Pick<T, K> - select properties

Pick<T, K> creates a new type containing only the selected properties of T. The second parameter is a list of field names joined with |:

1// Only name and species from the full Dinosaur interface
2type DinosaurPreview = Pick<Dinosaur, 'name' | 'species'>;
3
4// DinosaurPreview = { name: string; species: string; }
5
6const preview: DinosaurPreview = {
7  name: 'Blue',
8  species: 'Velociraptor'
9};
10
11// Useful e.g. for a list displayed on screen
12function renderDinosaurList(dinosaurs: DinosaurPreview[]): void {
13  dinosaurs.forEach(dino => {
14    console.log(`${dino.name} (${dino.species})`);
15  });
16}

Pick checks that the given keys exist: Pick<Dinosaur, 'enclosureID'> with a typo ends in a compile error right away. The field types carry over unchanged, so name is still a string.

Omit<T, K> - omit properties

Omit<T, K> works the opposite way to Pick and creates a new type without the selected properties:

1// Dinosaur without enclosureId (e.g. for a newly discovered species)
2type DinosaurWithoutEnclosure = Omit<Dinosaur, 'enclosureId'>;
3
4const newDiscovery: DinosaurWithoutEnclosure = {
5  name: 'New species',
6  species: 'Unknownus rex',
7  dangerLevel: 5,
8  dietType: 'carnivore'
9  // we do not need to provide enclosureId
10};
11
12// Omit multiple fields at once
13type DinosaurBasicInfo = Omit<Dinosaur, 'enclosureId' | 'dangerLevel'>;

Omit does not remove anything from existing objects; it only describes a new shape. It also has a trap: unlike Pick, it does not check whether a key exists, so Omit<Dinosaur, 'enclosureID'> passes without an error and omits nothing. That is why, when you keep only a few fields, I recommend Pick.

Record<K, V> - typed dictionary

Record<K, V> creates an object type whose keys have type K and whose values have type V:

1// Dictionary: zone name -> list of dinosaurs
2type ZoneMap = Record<string, Dinosaur[]>;
3
4const parkZones: ZoneMap = {
5  'Zone-A': [{ name: 'Rex', species: 'T-Rex', dangerLevel: 10, enclosureId: 'E-1', dietType: 'carnivore' }],
6  'Zone-B': [{ name: 'Trike', species: 'Triceratops', dangerLevel: 3, enclosureId: 'E-2', dietType: 'herbivore' }]
7};
8
9// Record with limited keys (union type)
10type DietCount = Record<'herbivore' | 'carnivore' | 'omnivore', number>;
11
12const census: DietCount = {
13  herbivore: 45,
14  carnivore: 12,
15  omnivore: 8
16};

With a string key, the dictionary accepts any zone name. With a union of keys, every one of them is mandatory, so if omnivore were missing from the census, the compiler would report an error. It is a great way to make sure no category is forgotten.

Readonly<T> - immutable type

Readonly<T> makes all properties read-only:

1type FrozenDinosaur = Readonly<Dinosaur>;
2
3const frozenDNA: FrozenDinosaur = {
4  name: 'Amber Specimen',
5  species: 'Unknown',
6  dangerLevel: 0,
7  enclosureId: 'LAB-1',
8  dietType: 'herbivore'
9};
10
11// frozenDNA.name = 'Changed'; // Error! Cannot assign to 'name' because it is a read-only property

Unlike Object.freeze() from the lesson on prototypes, this protection works only at compile time. In running JavaScript the object remains an ordinary object. Readonly is also shallow and does not protect nested objects.

Combining utility types

The real power comes from combining several utility types. Read such types from the inside out:

1// Dinosaur creation form: all fields required EXCEPT enclosureId
2type CreateDinosaurForm = Omit<Dinosaur, 'enclosureId'> & {
3  enclosureId?: string; // optional when creating
4};
5
6// Edit form: only selected fields, all optional
7type EditDinosaurForm = Partial<Pick<Dinosaur, 'name' | 'dangerLevel' | 'enclosureId'>>;
8
9// Immutable preview with selected fields
10type DinosaurReadonlyPreview = Readonly<Pick<Dinosaur, 'name' | 'species' | 'dangerLevel'>>;
11
12const readonlyPreview: DinosaurReadonlyPreview = {
13  name: 'Blue',
14  species: 'Velociraptor',
15  dangerLevel: 8
16};
17// readonlyPreview.dangerLevel = 10; // Error!

EditDinosaurForm first selects three fields and then makes them optional. In CreateDinosaurForm the & sign is an intersection: it combines the type without enclosureId with an object in which that field is optional.

Summary

TypeScript Utility Types allow creating variants of existing types without code duplication:

TypeAction
Partial<T>All fields optional
Required<T>All fields required
Pick<T, K>Select specific fields
Omit<T, K>Omit specific fields
Record<K, V>Typed dictionary
Readonly<T>All fields read-only

That is not the whole toolbox: the TypeScript documentation also describes ReturnType, Parameters, Exclude and NonNullable, among others. As Dr. Wu says: "Don't create 10 separate interfaces when one base type and utility types will handle it elegantly and safely!" In the next lesson we will look at TypeScript configuration and the tools around the compiler.

Practice

The sandbox contains the Dinosaur interface with Partial and Pick examples. Add a variant with Omit and deliberately make a typo in a key to see the difference between Pick and Omit.

Code for this lesson: index.ts
1// TypeScript Utility Types
2// Dinosaur management system in Jurassic Park
3
4// Base dinosaur interface
5interface Dinosaur {
6  name: string;
7  species: string;
8  dangerLevel: number;
9  enclosureId: string;
10  dietType: "herbivore" | "carnivore" | "omnivore";
11}
12
13// === Partial<T> - all fields optional ===
14type DinosaurUpdate = Partial<Dinosaur>;
15
16function updateDinosaur(id: string, changes: DinosaurUpdate): void {
17  console.log("Updating dinosaur " + id + ":", changes);
18}
19
20updateDinosaur("DINO-001", { dangerLevel: 9 });
21updateDinosaur("DINO-002", { name: "Blue", enclosureId: "E-7" });
22
23// === Pick<T, K> - pick properties ===
24type DinosaurPreview = Pick<Dinosaur, "name" | "species">;
25
26const preview: DinosaurPreview = {
27  name: "Blue",
28  species: "Velociraptor",
29};
30console.log("\nPreview:", preview);
31
32// === Omit<T, K> - omit properties ===
33type DinosaurWithoutEnclosure = Omit<Dinosaur, "enclosureId">;
34
35const newDiscovery: DinosaurWithoutEnclosure = {
36  name: "New species",
37  species: "Unknownus rex",
38  dangerLevel: 5,
39  dietType: "carnivore",
40};
41console.log("\nNew discovery:", newDiscovery);
42
43// === Record<K, V> - a typed dictionary ===
44type DietCount = Record<"herbivore" | "carnivore" | "omnivore", number>;
45
46const census: DietCount = {
47  herbivore: 45,
48  carnivore: 12,
49  omnivore: 8,
50};
51console.log("\nCensus:", census);
52
53// === Readonly<T> - an immutable type ===
54type FrozenDinosaur = Readonly<Dinosaur>;
55
56const frozen: FrozenDinosaur = {
57  name: "Amber Specimen",
58  species: "Unknown",
59  dangerLevel: 0,
60  enclosureId: "LAB-1",
61  dietType: "herbivore",
62};
63console.log("\nFrozen:", frozen);
64// frozen.name = "Changed"; // Error! Read-only
65
66// === Combining utility types ===
67type EditForm = Partial<Pick<Dinosaur, "name" | "dangerLevel" | "enclosureId">>;
68
69const edit: EditForm = { dangerLevel: 7 };
70console.log("\nEdit form:", edit);

Remember: the base type is the original DNA sample, and utility types breed variants from it, so a fix in the sample reaches every line at once.

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. Which utility type in TypeScript makes ALL object properties optional?

  2. 2. What does the Omit<T, K> type do in TypeScript?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Use Pick, Omit, and Partial on the Dinosaur interface to create type variants.

  • Horizontal ordering

    Arrange the elements of the Pick utility type usage:

  • Vertical ordering

    Order TypeScript concepts from simplest to most complex:

Useful articles