TypeScript - Mapped Types: A Beginner's Guide
Hello there, future TypeScript wizards! Today, we're going to embark on an exciting journey into the world of Mapped Types. Don't worry if you're new to programming – I'll be your friendly guide, and we'll take this step by step. By the end of this tutorial, you'll be mapping types like a pro!
What are Mapped Types?
Before we dive in, let's understand what Mapped Types are. Imagine you have a box of assorted chocolates, and you want to create a new box where every chocolate is wrapped in gold foil. That's essentially what Mapped Types do in TypeScript – they take an existing type and transform it into a new type based on a set of rules.
Built-in Mapped Types
TypeScript comes with some pre-defined Mapped Types that are super useful. Let's look at them one by one:
1. Partial
The Partial<T>
type makes all properties of T
optional. It's like turning a strict recipe into a flexible one where you can skip some ingredients.
interface Recipe {
name: string;
ingredients: string[];
cookingTime: number;
}
type FlexibleRecipe = Partial<Recipe>;
// Now we can create a recipe without all properties
const quickSnack: FlexibleRecipe = {
name: "Toast",
// We can skip ingredients and cookingTime
};
In this example, FlexibleRecipe
allows us to create a recipe without specifying all the properties. It's perfect when you want to update just a part of an object.
2. Required
Required<T>
is the opposite of Partial<T>
. It makes all properties required, even if they were optional in the original type.
interface UserProfile {
name: string;
age?: number;
email?: string;
}
type CompleteUserProfile = Required<UserProfile>;
// Now we must provide all properties
const user: CompleteUserProfile = {
name: "Alice",
age: 30,
email: "[email protected]"
};
Here, CompleteUserProfile
ensures that we provide all properties, including age
and email
which were optional in the original UserProfile
.
3. Readonly
Readonly<T>
makes all properties of T
read-only. It's like putting your type in a glass case – you can look, but you can't touch!
interface Toy {
name: string;
price: number;
}
type CollectibleToy = Readonly<Toy>;
const actionFigure: CollectibleToy = {
name: "Superhero",
price: 19.99
};
// This will cause an error
// actionFigure.price = 29.99;
In this example, once we create our actionFigure
, we can't modify its properties. It's great for creating immutable objects.
4. Pick<T, K>
Pick<T, K>
creates a new type by selecting only the specified properties K
from T
. It's like cherry-picking your favorite features from a type.
interface Smartphone {
brand: string;
model: string;
year: number;
color: string;
price: number;
}
type BasicPhoneInfo = Pick<Smartphone, 'brand' | 'model'>;
const myPhone: BasicPhoneInfo = {
brand: "TechBrand",
model: "X2000"
};
Here, BasicPhoneInfo
only includes the brand
and model
properties from Smartphone
, leaving out the rest.
5. Omit<T, K>
Omit<T, K>
is the opposite of Pick<T, K>
. It creates a new type by removing the specified properties K
from T
.
interface Book {
title: string;
author: string;
pages: number;
isbn: string;
}
type BookPreview = Omit<Book, 'pages' | 'isbn'>;
const preview: BookPreview = {
title: "TypeScript Adventures",
author: "Code Wizard"
};
In this case, BookPreview
includes all properties of Book
except pages
and isbn
.
Examples of Using Mapped Types
Now that we've seen the built-in Mapped Types, let's look at some practical examples of how we can use them in real-world scenarios.
Example 1: Creating a Form State
Imagine you're building a form, and you want to keep track of which fields have been modified:
interface LoginForm {
username: string;
password: string;
rememberMe: boolean;
}
type FormTouched = { [K in keyof LoginForm]: boolean };
const touchedFields: FormTouched = {
username: true,
password: false,
rememberMe: true
};
Here, we've created a FormTouched
type that has the same keys as LoginForm
, but all values are booleans indicating whether the field has been touched.
Example 2: API Response Wrapper
Let's say you have an API that returns different types of data, and you want to wrap each response in a standard format:
interface ApiResponse<T> {
data: T;
status: 'success' | 'error';
timestamp: number;
}
type UserData = { id: number; name: string; email: string };
type ProductData = { id: number; name: string; price: number };
const userResponse: ApiResponse<UserData> = {
data: { id: 1, name: "John Doe", email: "[email protected]" },
status: 'success',
timestamp: Date.now()
};
const productResponse: ApiResponse<ProductData> = {
data: { id: 101, name: "Laptop", price: 999.99 },
status: 'success',
timestamp: Date.now()
};
This example shows how we can use generics with Mapped Types to create flexible, reusable type structures.
Creating Custom Mapped Types
Now, let's flex our TypeScript muscles and create some custom Mapped Types!
Custom Type 1: Nullable
Let's create a type that makes all properties nullable:
type Nullable<T> = { [K in keyof T]: T[K] | null };
interface Person {
name: string;
age: number;
}
type NullablePerson = Nullable<Person>;
const maybePerson: NullablePerson = {
name: "Jane",
age: null // This is now valid
};
Our Nullable<T>
type allows any property to be either its original type or null
.
Custom Type 2: Freezable
Let's create a type that adds a freeze
method to an object:
type Freezable<T> = T & { freeze(): Readonly<T> };
interface Config {
theme: string;
fontSize: number;
}
function makeFreezable<T>(obj: T): Freezable<T> {
return {
...obj,
freeze() {
return Object.freeze({ ...this }) as Readonly<T>;
}
};
}
const config = makeFreezable<Config>({
theme: "dark",
fontSize: 14
});
const frozenConfig = config.freeze();
// frozenConfig.theme = "light"; // This would cause an error
This custom type adds a freeze
method to any object, allowing us to create an immutable version of it.
Conclusion
Wow, we've covered a lot of ground today! From built-in Mapped Types to creating our own custom types, you've seen how powerful and flexible TypeScript can be. Mapped Types are like magic wands in your TypeScript toolkit – they allow you to transform and manipulate types in incredibly useful ways.
Remember, the key to mastering Mapped Types is practice. Try creating your own, experiment with different combinations, and soon you'll be writing TypeScript code that's not just functional, but elegant and type-safe too.
Keep coding, keep learning, and most importantly, have fun with TypeScript!
Mapped Type | Description | Example |
---|---|---|
Partial |
Makes all properties optional | type PartialUser = Partial<User> |
Required |
Makes all properties required | type RequiredUser = Required<User> |
Readonly |
Makes all properties read-only | type ReadonlyUser = Readonly<User> |
Pick<T, K> | Creates a type with only the specified properties | type UserName = Pick<User, 'name'> |
Omit<T, K> | Creates a type without the specified properties | type UserWithoutPassword = Omit<User, 'password'> |
Record<K, T> | Creates an object type with keys of K and values of T | type StringMap = Record<string, string> |
Exclude<T, U> | Excludes types in U from T | type NumberOnly = Exclude<number | string, string> |
Extract<T, U> | Extracts types in U from T | type StringOnly = Extract<number | string, string> |
NonNullable |
Removes null and undefined from T | type NonNullableString = NonNullable<string | null | undefined> |
Parameters |
Extracts parameter types of a function type | type Params = Parameters<(a: number, b: string) => void> |
ReturnType |
Extracts the return type of a function type | type ReturnString = ReturnType<() => string> |
InstanceType |
Extracts the instance type of a constructor function type | type DateInstance = InstanceType<typeof Date> |
Credits: Image by storyset