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.


_10
type 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:


_10
type CanFly<T> = T extends { wings: true } ? "Can fly" : "Cannot fly";
_10
_10
type Bird = { wings: true };
_10
type Dog = { wings: false };
_10
_10
type BirdCanFly = CanFly<Bird>; // "Can fly"
_10
type 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:


_10
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
_10
_10
type ExampleFunc = (name: string, age: number) => void;
_10
_10
type 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.


_10
type 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


_12
type ReadOnly<T> = {
_12
readonly [Key in keyof T]: T[Key];
_12
};
_12
_12
type Pet = { name: string; age: number };
_12
type ReadOnlyPet = ReadOnly<Pet>;
_12
_12
// Equivalent to:
_12
type ReadOnlyPet = {
_12
readonly name: string;
_12
readonly age: number;
_12
};

Mapped types allow us to simplify repetitive tasks like adding modifiers.

Renaming Keys

Using the as keyword, we can transform keys dynamically


_12
type RenameKeys<T> = {
_12
[Key in keyof T as `new_${string & Key}`]: T[Key];
_12
};
_12
_12
type OldKeys = { a: string; b: number };
_12
type NewKeys = RenameKeys<OldKeys>;
_12
_12
// Equivalent to:
_12
type 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:


_10
type NestedArray<T> = T | NestedArray<T[]>;
_10
_10
type Example = NestedArray<string>;
_10
// Example can be:
_10
string | 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:


_12
type JSONValue = string | number | boolean | null | JSONArray | JSONObject;
_12
type JSONArray = JSONValue[];
_12
type JSONObject = { [key: string]: JSONValue };
_12
_12
const 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:


_24
type DeepNullable<T> = {
_24
[Key in keyof T]: T[Key] extends object
_24
? DeepNullable<T[Key]>
_24
: T[Key] | null;
_24
};
_24
_24
type Pet = {
_24
name: string;
_24
traits: {
_24
canFly: boolean;
_24
canBark: boolean;
_24
};
_24
};
_24
_24
type NullablePet = DeepNullable<Pet>;
_24
_24
// Equivalent to:
_24
type 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.

1) What does a conditional type T extends U ? X : Y resolve to if T does NOT extend U?

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?

D is for danny

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.

Gobacktothetop

Made with 🥰 in 🏴󠁧󠁢󠁥󠁮󠁧󠁿

©2025 All rights reserved.