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?" Fortunately, TypeScript offers Utility Types - ready-made transformations that create new types based on existing ones.
Utility Types are generic types built into TypeScript that allow you to transform existing types into new ones. Instead of writing separate interfaces for each variant, you can use one base type and create derived types from it.
Partial<T> makes all properties of type T optional. Ideal for updates where we don't want to require all fields to be provided: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're changing
18updateDinosaur('DINO-001', { dangerLevel: 9 });
19updateDinosaur('DINO-002', { name: 'Blue', enclosureId: 'E-7' });Required<T> is the opposite of Partial - it makes all properties (even optional ones) 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};Pick<T, K> creates a new type containing only selected properties from T: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}Omit<T, K> is the opposite of Pick - it creates a new type without 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 don't need to provide enclosureId
10};
11
12// Omit multiple fields at once
13type DinosaurBasicInfo = Omit<Dinosaur, 'enclosureId' | 'dangerLevel'>;Record<K, V> creates an object type where keys have type K and 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};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 propertyThe real power comes from combining several utility types:
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!TypeScript Utility Types allow creating variants of existing types without code duplication:
| Type | Action | |------|--------| |
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 |As Dr. Wu says: "Don't create 10 separate interfaces when one base type and utility types will handle it elegantly and safely!"