TypeScript Functions

 

Advanced Type Mapping in TypeScript

TypeScript, the statically typed superset of JavaScript, has gained immense popularity among developers for its ability to catch errors early and provide enhanced tooling support. One of the standout features of TypeScript is its advanced type system, which allows developers to create sophisticated type mappings and manipulate types with precision. In this blog post, we will delve into the world of advanced type mapping in TypeScript, exploring various techniques and use cases that can greatly enhance the flexibility and robustness of your code.

Advanced Type Mapping in TypeScript

1. Understanding Type Mapping:

1.1 Basic Type Mapping:

TypeScript allows us to map one type to another using the keyof operator and indexing types. This basic type mapping technique is useful for accessing properties dynamically and extracting specific property types from an object. Here’s an example:

typescript
type Person = {
  name: string;
  age: number;
  email: string;
};

type PersonKeys = keyof Person;
// Result: "name" | "age" | "email"

1.2 Advanced Type Mapping:

Advanced type mapping techniques go beyond basic mappings and enable us to transform types in various powerful ways. These techniques often involve the use of conditional types and utility types, which we’ll explore in detail later in this blog post.

2. Using Mapped Types:

Mapped types provide a concise way to define new types based on existing ones. They allow us to modify the properties of a type in a consistent manner. Let’s take a look at some commonly used mapped types:

2.1 Readonly:

The Readonly mapped type transforms all properties of an object type to be read-only. This ensures that the properties cannot be modified once the object is created. Here’s an example:

typescript
type ReadonlyPerson = Readonly<Person>;
// Result: {
//   readonly name: string;
//   readonly age: number;
//   readonly email: string;
// }

2.2 Partial:

The Partial mapped type allows us to create a new type with all properties of the original type set as optional. This can be handy when dealing with forms or optional arguments. Here’s an example:

typescript
type PartialPerson = Partial<Person>;
// Result: {
//   name?: string;
//   age?: number;
//   email?: string;
// }

2.3 Required:

Conversely, the Required mapped type transforms all optional properties of an object type into required properties. This can be useful when we want to ensure that all properties are present. Here’s an example:

typescript
type RequiredPerson = Required<PartialPerson>;
// Result: {
//   name: string;
//   age: number;
//   email: string;
// }

2.4 Pick:

The Pick mapped type allows us to select specific properties from an existing type to create a new type. This is particularly useful when we only need a subset of properties from a larger type. Here’s an example:

typescript
type PersonNameAndAge = Pick<Person, "name" | "age">;
// Result: {
//   name: string;
//   age: number;
// }

2.5 Omit:

Similar to Pick, the Omit mapped type creates a new type by excluding specified properties from an existing type. This can be handy when we want to remove certain properties from a type. Here’s an example:

typescript
type PersonWithoutAge = Omit<Person, "age">;
// Result: {
//   name: string;
//   email: string;
// }

2.6 Record:

The Record mapped type allows us to create an object type with specified keys and a common value type. This can be useful when creating dictionaries or lookup tables. Here’s an example:

typescript
type PersonRecord = Record<"id1" | "id2", Person>;
// Result: {
//   id1: Person;
//   id2: Person;
// }

3. Conditional Types:

Conditional types in TypeScript enable us to create types that depend on a condition. These types can be defined using the extends keyword, allowing us to perform type-level branching. Let’s explore some aspects of conditional types:

3.1 Basics of Conditional Types:

Conditional types use the extends keyword and infer to branch the type based on conditions. Here’s an example of a basic conditional type that checks if a type is an array:

typescript
type IsArray<T> = T extends Array<any> ? true : false;

type Result = IsArray<string[]>;
// Result: true

type AnotherResult = IsArray<number>;
// Result: false

3.2 Conditional Type Inference:

The infer keyword allows us to infer a type from another type and use it within a conditional type. This can be extremely useful when working with generic types. Here’s an example:

typescript
type ArrayElementType<T> = T extends Array<infer U> ? U : never;

type Element = ArrayElementType<string[]>;
// Result: string

type AnotherElement = ArrayElementType<number[]>;
// Result: number

3.3 Distributive Conditional Types:

Distributive conditional types distribute over union types, making them powerful tools for type manipulation. Here’s an example that showcases the distributive behavior:

typescript
type ToArray<T> = T extends any ? T[] : T[];

type StringArray = ToArray<string>;
// Result: string[]

type UnionArray = ToArray<string | number>;
// Result: (string | number)[]

4. Utility Types:

TypeScript provides a set of built-in utility types that simplify common type transformations. Let’s explore a few commonly used utility types:

4.1 NonNullable:

The NonNullable utility type removes null and undefined from a type. This ensures that the resulting type is guaranteed to have non-nullable values. Here’s an example:

typescript
type NullableString = string | null | undefined;
type NonNullableString = NonNullable<NullableString>;
// Result: string

4.2 Extract and Exclude:

The Extract and Exclude utility types allow us to filter union types based on specified criteria. Extract selects the types that are assignable to a given type, while Exclude excludes the types that are assignable to a given type. Here’s an example:

typescript
type AllowedValues = "A" | "B" | "C";
type FilteredValues = Extract<AllowedValues, "A" | "B">;
// Result: "A" | "B"

type ExcludedValues = Exclude<AllowedValues, "A" | "B">;
// Result: "C"

4.3 ReturnType and Parameters:

The ReturnType utility type extracts the return type of a function, while the Parameters utility type extracts the parameter types of a function. These types are particularly useful when working with higher-order functions. Here’s an example:

typescript
type Add = (a: number, b: number) => number;

type AddReturnType = ReturnType<Add>;
// Result: number

type AddParameters = Parameters<Add>;
// Result: [number, number]

4.4 Required and Partial (Again!):

We’ve already seen the Required and Partial mapped types, but TypeScript also provides utility types with the same names. These utility types work similarly to their mapped type counterparts but provide more flexibility when used in generic contexts. Here’s an example:

typescript
type Person = {
  name: string;
  age?: number;
};

type RequiredPerson = Required<Person>;
// Result: {
//   name: string;
//   age: number;
// }

type PartialPerson = Partial<Person>;
// Result: {
//   name?: string;
//   age?: number;
// }

5. Advanced Techniques:

5.1 Key Remapping:

TypeScript allows us to remap keys of a type using mapped types and conditional types. This can be useful when we want to transform the shape of a type while preserving its content. Here’s an example that remaps keys from snake_case to camelCase:

typescript
type CamelCase<S extends string> = S extends `${infer L}_${infer R}`
  ? `${L}${Capitalize<CamelCase<R>>}`
  : S;

type SnakeCaseToCamelCase<T> = {
  [K in keyof T as CamelCase<string & K>]: T[K];
};

type SnakeCasePerson = {
  first_name: string;
  last_name: string;
  email_address: string;
};

type CamelCasePerson = SnakeCaseToCamelCase<SnakeCasePerson>;
// Result: {
//   firstName: string;
//   lastName: string;
//   emailAddress: string;
// }

5.2 Recursive Type Mappings:

TypeScript’s advanced type system allows us to create recursive type mappings, enabling us to work with deeply nested structures. This can be useful when dealing with complex data structures or recursive algorithms. Here’s an example that applies a transformation to all string values within an object:

typescript
type RecursiveStringTransformation<T> = {
  [K in keyof T]: T[K] extends string
    ? Uppercase<T[K]>
    : RecursiveStringTransformation<T[K]>;
};

type NestedObject = {
  name: string;
  address: {
    street: string;
    city: string;
  };
};

type TransformedObject = RecursiveStringTransformation<NestedObject>;
// Result: {
//   name: string;
//   address: {
//     street: string;
//     city: string;
//   };
// }

5.3 Combining Techniques:

By combining various advanced type mapping techniques, we can create powerful and expressive type definitions. These techniques can be used to create highly reusable and flexible code, enabling us to write more robust and maintainable applications.

Conclusion:

In this blog post, we have explored the world of advanced type mapping in TypeScript. We started by understanding the basics of type mapping and then dived into the powerful features provided by mapped types, conditional types, and utility types. We also explored advanced techniques such as key remapping and recursive type mappings. Armed with this knowledge, you can now leverage TypeScript’s advanced type system to create flexible, robust, and type-safe code.

Previously at
Flag Argentina
Argentina
time icon
GMT-3
Experienced software engineer with a passion for TypeScript and full-stack development. TypeScript advocate with extensive 5 years experience spanning startups to global brands.