TypeScript Working with Mapped Types

TypeScript is a statically typed superset of JavaScript that brings a powerful type system to the JavaScript ecosystem. One of the advanced and useful features in TypeScript is mapped types. Mapped types allow developers to create new types by transforming each property in an existing type. This is extremely valuable when you need to perform operations like making all properties optional, readonly, or transforming their types based on some rules. In this blog post, we will explore the fundamental concepts of working with mapped types in TypeScript, their usage methods, common practices, and best practices.

Table of Contents

  1. Fundamental Concepts of Mapped Types
  2. Usage Methods
  3. Common Practices
  4. Best Practices
  5. Conclusion
  6. References

Fundamental Concepts of Mapped Types

At its core, a mapped type in TypeScript uses a property signature with a for...in - like syntax to iterate over the properties of an existing type and create a new type. The basic syntax of a mapped type is as follows:

type MappedType<OriginalType> = {
    [Property in keyof OriginalType]: NewPropertyType;
};
  • OriginalType is the type whose properties we want to iterate over.
  • Property is a variable that represents each property name in the OriginalType during the iteration.
  • keyof OriginalType is a type query that returns a union of all property names in the OriginalType.
  • NewPropertyType is the type that we assign to each property in the new type.

Let’s look at a simple example:

// Original type
type User = {
    name: string;
    age: number;
};

// Mapped type to make all properties optional
type OptionalUser = {
    [Property in keyof User]?: User[Property];
};

const optionalUser: OptionalUser = {
    name: 'John'
};

In this example, we create a new type OptionalUser from the User type. The OptionalUser type has the same properties as User, but all of them are optional.

Usage Methods

Making Properties Optional or Readonly

We can use mapped types to make all properties in a type optional or readonly.

// Original type
type Person = {
    firstName: string;
    lastName: string;
    age: number;
};

// Make all properties optional
type OptionalPerson = {
    [Property in keyof Person]?: Person[Property];
};

// Make all properties readonly
type ReadonlyPerson = {
    readonly [Property in keyof Person]: Person[Property];
};

const optionalPerson: OptionalPerson = {
    firstName: 'Jane'
};

const readonlyPerson: ReadonlyPerson = {
    firstName: 'Bob',
    lastName: 'Smith',
    age: 30
};

// This will cause a compilation error because the property is readonly
// readonlyPerson.age = 31;

Transforming Property Types

We can also transform the types of properties in a type. For example, we can convert all string properties to numbers.

type StringToNumber<Type> = {
    [Property in keyof Type]: Type[Property] extends string ? number : Type[Property];
};

type Example = {
    name: string;
    age: number;
};

type TransformedExample = StringToNumber<Example>;

const transformed: TransformedExample = {
    name: 123,
    age: 25
};

Common Practices

Creating Partial and Readonly Utility Types

TypeScript already provides built - in utility types Partial<T> and Readonly<T> which are implemented using mapped types.

type Car = {
    make: string;
    model: string;
    year: number;
};

// Using Partial<T>
type PartialCar = Partial<Car>;

const partialCar: PartialCar = {
    make: 'Toyota'
};

// Using Readonly<T>
type ReadonlyCar = Readonly<Car>;

const readonlyCar: ReadonlyCar = {
    make: 'Honda',
    model: 'Civic',
    year: 2022
};

Filtering Properties

We can use mapped types to filter properties based on their types.

type OnlyStringProperties<Type> = {
    [Property in keyof Type as Type[Property] extends string ? Property : never]: Type[Property];
};

type MixedType = {
    name: string;
    age: number;
    address: string;
};

type StringProperties = OnlyStringProperties<MixedType>;

const stringProps: StringProperties = {
    name: 'Alice',
    address: '123 Main St'
};

Best Practices

Keep Mapped Types Simple and Readable

Mapped types can become complex quickly, especially when using conditional types and type queries. It’s important to keep the code simple and readable. If a mapped type becomes too complicated, consider breaking it down into smaller, more manageable types.

Use Built - in Utility Types

TypeScript provides several built - in utility types like Partial<T>, Readonly<T>, Pick<T, K>, and Omit<T, K> that are implemented using mapped types. Use these utility types whenever possible to avoid reinventing the wheel.

Document Mapped Types

If you create custom mapped types, document them clearly. Explain what the type does, what the input and output types are, and any assumptions or limitations.

Conclusion

Mapped types in TypeScript are a powerful feature that allows developers to create new types by transforming existing types. They can be used to make properties optional or readonly, transform property types, and filter properties. By understanding the fundamental concepts, usage methods, common practices, and best practices of mapped types, developers can write more robust and maintainable TypeScript code.

References