Sometimes, tackling complex data structures in TypeScript can leave you feeling at a loss. Whether you’re managing nested data structures or dynamically transforming object types, often basic type composition can leave you coming up short.
This is where Conditional Types, Mapped Types, and Recursive Types come to the rescue and prove to be indispensable tools that offer dynamic, type-safe solutions needed to tame the most complex challenges allowing you to handle them with elegance.
Conditional Types
#Conditional types allow us to express logic in TypeScript’s type system. They’re like if
statements for types, enabling our code to adapt based on type conditions.
_10type ConditionalType<T> = T extends U ? X : Y;
This translates to:
If T
extends U
, the type resolves to X
. Otherwise, it resolves to Y
.
Simple Type Logic
We can determine whether an animal can fly based on its traits:
_10type CanFly<T> = T extends { wings: true } ? "Can fly" : "Cannot fly";_10_10type Bird = { wings: true };_10type Dog = { wings: false };_10_10type BirdCanFly = CanFly<Bird>; // "Can fly"_10type DogCanFly = CanFly<Dog>; // "Cannot fly"
Here we’re using dynamic type behaviour to allow us to handle varying requirements efficiently.
Extracting Function Parameters
Conditional types are particularly useful when dealing with functions. Let’s extract the parameter types from a function:
_10type Parameters<T> = T extends (...args: infer P) => any ? P : never;_10_10type ExampleFunc = (name: string, age: number) => void;_10_10type FuncParams = Parameters<ExampleFunc>; // [string, number]
The infer
keyword helps extract types dynamically, making it ideal for scenarios like this.
Mapped Types
#Mapped types allow us to iterate over the keys within a object type and apply transformations this makes them perfect for dynamically modifying type structures.
_10type Mapped<T> = {_10 [Key in keyof T]: TransformedType;_10};
This creates a new type where each property in T
maps to a new type.
Adding Modifiers
Suppose we want to make all properties of an object readonly
_12type ReadOnly<T> = {_12 readonly [Key in keyof T]: T[Key];_12};_12_12type Pet = { name: string; age: number };_12type ReadOnlyPet = ReadOnly<Pet>;_12_12// Equivalent to:_12type ReadOnlyPet = {_12 readonly name: string;_12 readonly age: number;_12};
Info
readonly
is only a compile-time artefact, there's no protection against property assignments at runtime.
Mapped types allow us to simplify repetitive tasks like adding modifiers.
Renaming Keys
Using the as
keyword, we can transform keys dynamically
_12type RenameKeys<T> = {_12 [Key in keyof T as `new_${string & Key}`]: T[Key];_12};_12_12type OldKeys = { a: string; b: number };_12type NewKeys = RenameKeys<OldKeys>;_12_12// Equivalent to:_12type NewKeys = {_12 new_a: string;_12 new_b: number;_12};
This is useful for standardising key formats or adapting types to different APIs.
Recursive Types
#Recursive types let us define types that reference themselves. They’re essential for representing deeply nested or self-referential data structures.
Nested Arrays
Here’s a type that supports arrays of arbitrary depth:
_10type NestedArray<T> = T | NestedArray<T[]>;_10_10type Example = NestedArray<string>;_10// Example can be:_10string | string[] | string[][] | string[][][]
Recursive types elegantly represent these complex data structures.
JSON-like Structures
Suppose we’re handling JSON data. Recursive types allow us to define a JSON value type:
_12type JSONValue = string | number | boolean | null | JSONArray | JSONObject;_12type JSONArray = JSONValue[];_12type JSONObject = { [key: string]: JSONValue };_12_12const example: JSONObject = {_12 name: "Fido",_12 traits: {_12 friendly: true,_12 age: 4,_12 },_12 tags: ["playful", "energetic"],_12};
This type captures the recursive nature of JSON structures seamlessly.
Combining Conditional, Mapped, and Recursive Types
#The real magic happens when these features are combined. Let’s create a utility type that transforms an object by making all string properties nullable recursively:
_24type DeepNullable<T> = {_24 [Key in keyof T]: T[Key] extends object_24 ? DeepNullable<T[Key]>_24 : T[Key] | null;_24};_24_24type Pet = {_24 name: string;_24 traits: {_24 canFly: boolean;_24 canBark: boolean;_24 };_24};_24_24type NullablePet = DeepNullable<Pet>;_24_24// Equivalent to:_24type NullablePet = {_24 name: string | null;_24 traits: {_24 canFly: boolean | null;_24 canBark: boolean | null;_24 };_24};
This combines recursion with conditional and mapped types to create a highly adaptable type transformation.
Conclusion
#Conditional, mapped, and recursive types are the cornerstones of advanced TypeScript programming.
They allow us to handle complex data structures with ease as well as helping us to reduce boilerplate with reusable and composable type utilities while allowing types to adapt dynamically to your code’s needs, using these features will elevate your TypeScript skills to the next level.
As always; the best way to learn is by doing- start experimenting with these types in your projects today. Once you master them, you’ll wonder how you ever coded without them!
Reinforce Your Learning
One of the best ways to reinforce what your learning is to test yourself to solidify the knowlege in your memory.
Complete this 10 question quiz to see how much you remember.
Thanks alot for your feedback!
The insights you share really help me with improving the quality of the content here.
If there's anything you would like to add, please send a message to:
[email protected]Was this article this helpful?
About the author
Danny Engineering
A software engineer with a strong belief in human-centric design and driven by a deep empathy for users. Combining the latest technology with human values to build a better, more connected world.