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!

TypeScript - Mapped Types

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