Back to Python
2026-04-068 min read

TS Mapped Types (Python Programming)

Learn TS Mapped Types (Python Programming) step by step with clear examples and exercises.

Title: A full guide to TypeScript Mapped Types for Python Programmers

Why This Matters

TypeScript is a powerful superset of JavaScript that adds optional static typing to the language. While TypeScript is not directly related to Python, understanding mapped types can help you write more robust and maintainable code in both languages. In this lesson, we'll explore TypeScript mapped types, their practical applications, and how they can benefit Python programmers by providing a structured way to manipulate type definitions based on existing ones.

Prerequisites

To follow along with this guide, you should have a basic understanding of:

  • JavaScript or TypeScript syntax (variables, functions, loops, conditional statements, and control flow)
  • Object-oriented programming concepts (classes, inheritance, interfaces, and type hierarchies)
  • Familiarity with Python's object-oriented programming principles will also be helpful in understanding the parallels between TypeScript mapped types and Python's type hinting.
  • Basic knowledge of TypeScript syntax, such as interfaces, generics, and utility types, is recommended but not required.

Core Concept

Mapped Types in TypeScript allow you to define new types based on existing ones by applying transformations to their properties. This can help enforce specific structure and relationships between objects, making your code more flexible and easier to maintain.

Let's dive into a simple example:

interface Person {
name: string;
age: number;
}

type ReadOnly<T> = {
readonly [P in keyof T]: T[P];
};

const person: Person = { name: 'Alice', age: 30 };
const readOnlyPerson: ReadOnly<Person> = { ...person, readonly: true };

In this example, we define an interface Person with two properties: name and age. We then create a mapped type called ReadOnly, which makes all properties of the provided type (in this case Person) read-only. By using the [P in keyof T] syntax, TypeScript automatically generates a new property for each key in the original type.

The readOnlyPerson variable is created by spreading the original person object into the ReadOnly type, effectively making all properties read-only. This can help prevent accidental modifications to sensitive data and improve overall code safety.

Mapped Types with Interfaces and Classes

Mapped types can also be used with interfaces and classes to enforce specific structures and relationships between objects:

interface Vehicle {
brand: string;
model: string;
year: number;
}

class Car implements Vehicle {
constructor(public brand: string, public model: string, public year: number) {}
}

type WithYearCheck<T> = T extends Vehicle ? T : never;

const car1: Car = new Car('Toyota', 'Corolla', 2021);
const car2: WithYearCheck<Car> = car1; // This will work since car1 has a year property

interface Bike {
brand: string;
model: string;
}

class Motorcycle implements Bike {
constructor(public brand: string, public model: string) {}
}

const bike1: Bike = new Motorcycle('Honda', 'CBR');
const bike2: WithYearCheck<Bike> = bike1; // This will fail since bike1 does not have a year property

In this example, we define an interface Vehicle and create a class Car that implements it. We then create a mapped type called WithYearCheck, which can only accept objects that extend the Vehicle interface (in this case, instances of the Car class). By using this mapped type, we can ensure that only objects with a year property are accepted by the car2 variable.

Mapped Types with Generics

Mapped types can also be used with generics to create more flexible and reusable type definitions:

type KeysOf<T> = { [P in keyof T]: P };
type ValuesOf<T> = { [P in keyof T]: T[P] };

interface Person {
name: string;
age: number;
}

type PersonProperties = KeysOf<Person>; // 'name' | 'age'
type PersonValues = ValuesOf<Person>; // string | number

In this example, we create utility types KeysOf and ValuesOf, which can be used to extract the keys and values of an object's type, respectively. These utility types can be useful when working with mapped types to manipulate properties based on their types.

Worked Example

Let's extend our previous example to create a more complex scenario:

interface Vehicle {
brand: string;
model: string;
year: number;
}

type WithYearCheck<T> = T extends Vehicle ? T : never;

const car1: Vehicle = { brand: 'Toyota', model: 'Corolla', year: 2021 };
const car2: WithYearCheck<Vehicle> = car1; // This will work since car1 has a year property

interface Bike {
brand: string;
model: string;
}

type BikeWithYear<T extends Bike & { year: number }> = T;
const bike1: Bike = { brand: 'Honda', model: 'CBR' };
const bike2: BikeWithYear<Bike & { year: 2020 }> = { ...bike1, year: 2020 }; // This creates a new bike object with an added year property

In this example, we create a mapped type called BikeWithYear, which extends the Bike interface and adds a year property. By using conditional types (extends) and mapped types together, we can ensure that only objects with both a year property and the correct structure are accepted by the bike2 variable.

Common Mistakes

  1. Forgetting to extend the base type when defining mapped types:
type ReadOnly<T> = {
readonly name: string; // This should be T[P] instead of hardcoding 'name'
};
  1. Using any instead of mapped types to achieve similar functionality:
const readOnlyPerson: { ...person, readonly: true } = { ...person }; // This is not a mapped type and will not enforce read-only properties at compile time
  1. Misusing mapped types for simple data validation:
type ValidEmail<T> = T extends { email: string } ? T : never;
const invalidEmail: ValidEmail<{ name: string, age: number }> = { name: 'John', age: 25 }; // This will not prevent errors related to the email property since it's not part of the base type
  1. Assuming that mapped types can only be used with interfaces or classes:
type ReadOnlyArray<T> = T extends Array<any> ? { readonly [index in number]: T[number] } : never;
const readOnlyNumbers: ReadOnlyArray<number[]> = [1, 2, 3]; // This creates a read-only array of numbers

Common Mistakes (Continued)

  1. Forgetting to handle optional properties when using mapped types:
type Optional<T> = { [P in keyof T]?: T[P] };
const optionalPerson: Optional<Person> = { name: 'Alice', age: 30, favoriteColor?: string }; // This allows for an optional favoriteColor property

// To handle optional properties, you can use the `keyof T & keyof any` syntax to check if a property exists in the base type or in TypeScript's global object (which includes all JavaScript built-in objects). For example:
type OptionalWithCheck<T> = { [P in keyof T | keyof any]?: T[P] };
const optionalPersonWithCheck: OptionalWithCheck<Person> = { name: 'Alice', age: 30, favoriteColor?: string }; // This will only accept properties from the Person interface
  1. Misusing mapped types for complex data validation or business logic:

Mapped types are primarily used to create new type definitions based on existing ones. While it's possible to use them for complex data validation or business logic, it's generally recommended to keep these concerns separate and use other TypeScript features like interfaces, classes, and custom validators instead.

Practice Questions

  1. Create a mapped type called HasLength that checks if an object (of type T) has a length property. If it does, return the original object; otherwise, throw an error.
type HasLength<T> = T extends { length: number } ? T : never;
const array1: HasLength<number[]> = [1, 2, 3]; // This will work since array1 has a length property
const string1: HasLength<string> = 'Hello'; // This will work since string1 has a length property
const object1: HasLength<{ name: string }> = { name: 'Alice' }; // This will fail since object1 does not have a length property
  1. Given the following interfaces:
interface Animal {
name: string;
}

interface Dog extends Animal {
breed: string;
}

interface Cat extends Animal {
color: string;
}

Create a mapped type called Pets that can accept either a Dog or a Cat.

type Pets<T extends Animal> = T extends Dog ? T : T extends Cat ? T : never;
const pet1: Pets<Dog> = { name: 'Fido', breed: 'Labrador' }; // This will work since pet1 is a Dog
const pet2: Pets<Cat> = { name: 'Whiskers', color: 'Grey' }; // This will work since pet2 is a Cat
const pet3: Pets<Animal> = { name: 'Snowball' }; // This will work since pet3 is an Animal

FAQ

Q: Can I use mapped types with arrays?

A: Yes, you can create mapped types for arrays by using array literals and spreading operators. For example:

type ReadOnlyArray<T> = T extends Array<any> ? { readonly [index in number]: T[number] } : never;
const readOnlyNumbers: ReadOnlyArray<number[]> = [1, 2, 3]; // This creates a read-only array of numbers

Q: Are mapped types only available in TypeScript?

A: No, while TypeScript is the most popular language that supports mapped types, similar concepts can be found in other statically typed languages like Rust and Swift. However, the syntax may vary between these languages.

Q: How do I handle optional properties when using mapped types?

A: To handle optional properties, you can use the keyof T & keyof any syntax to check if a property exists in the base type or in TypeScript's global object (which includes all JavaScript built-in objects). For example:

type Optional<T> = { [P in keyof T | keyof any]?: T[P] };
const optionalPerson: Optional<Person> = { name: 'Alice', age: 30, favoriteColor?: string }; // This allows for an optional favoriteColor property

Q: Can I use mapped types to enforce method signatures or class inheritance?

A: Yes, TypeScript's mapped types can be used to create interfaces that enforce specific method signatures and class hierarchies. However, this is beyond the scope of this guide and requires a deeper understanding of TypeScript's interface and class syntax. For more information on this topic, refer to the official TypeScript documentation (https://www.typescriptlang.org/docs/handbook/2/interfaces.html).

Q: How do I use mapped types with union types?

A: You can use mapped types with union types by applying them to each member of the union individually or using conditional types (extends) to check if a given type belongs to the union. For example:

type Union<T, U> = T | U;
type ReadOnlyUnion<T, U> = T extends Union<infer A, infer B> ? { readonly [P in keyof A]: A[P] } : never;
const union1: ReadOnlyUnion<number[], string[]> = [1, 2, 3]; // This will work since union1 is a number[] or string[]
TS Mapped Types (Python Programming) | Python | XQA Learn