DEV Community

A. Sharif
A. Sharif

Posted on

Notes on Advanced TypeScript: Transforming Types

Introduction

These notes should help in better understanding advanced TypeScript topics and might be helpful when needing to lookup up how to leverage TypeScript in a specific situation. All examples are based on TypeScript 4.6.

Transforming Types

There are situations where you have a defined type, but need to adapt some properties to work for a specific use case. Let's take the following example, where we have defined a Box type:

type Vec2 = { x: number; y: number };
type Box = {
    id: string;
    size: Vec2;
    location: Vec2;
    content: string;
    color: string;
};
Enter fullscreen mode Exit fullscreen mode

This Box type works well, except that we have a user interface, that allows the user to define the size, content, color and even location. The id property might not be defined yet, which prevents us from using the type as is. We need a way to tell our input, that the provided values are a Box with the id property being optional.
The following example will not work:

const defineBox = (box: Box) => {
   // some processing happening here
};
defineBox({
  content: "Content goes here",
  color: "green",
  location: {x: 100, y: 100},
  size: {x: 50, y: 50}
}); 
/** 
 * Fail: Property 'id' is missing in type 
 * '{ content: string; color: string; location: { x: number; 
 * . y: number; }; size: { x: number; y: number; }; }' 
 * but required in type 'Box'. 
 */
Enter fullscreen mode Exit fullscreen mode

TypeScript will complain that the property id is required in type Box. The good news ist that we can transform our Box type to work via defining our own MakeOptional type. By leveraging the built-in types Pick and Omit we can create a type that accepts a defined of keys that we can be converted to optional:

type MakeOptional<Type, Keys extends keyof Type> = 
  Omit<Type, Keys> & Pick<Partial<Type>, Keys>;
Enter fullscreen mode Exit fullscreen mode

Let's take a closer look at what is happening. First we use Omit to remove any keys from the original type and then we make our type partial via the Partial type and pick the previously excluded keys. By joining the two type operations, we can now use the newly created MakeOptional in our previous example.

type BoxIdOptional = MakeOptional<Box, "id">;
const defineBox = (box: BoxIdOptional) => {

};
defineBox({
  content: "Content goes here",
  color: "green",
  location: {x: 100, y: 100},
  size: {x: 50, y: 50}
});
Enter fullscreen mode Exit fullscreen mode

Our defineBox function works as expected now, no matter if the id is provided or not. This good already, but we can do even more type transformations as needed. Let's look at a couple of more scenarios.
We might want to convert all properties by type, for example we would like to convert all properties of type string to number. This can be achieved by defining our own ConvertTypeTo type:

type ConvertTypeTo<Type, From, To> = {
  [Key in keyof Type]: Required<Type>[Key] extends From ? To : Type[Key];
};
Enter fullscreen mode Exit fullscreen mode

By going through all the keys, we check if a key extends the From generic type and convert it to the defined To type.

/**
 * type BoxStringToNumber = {
 *   id: number;
 *   size: Vec2;
 *   location: Vec2;
 *   content: number;
 *   color: number;
 * }
 */
type BoxStringToNumber = ConvertTypeTo<Box, string, number>;
Enter fullscreen mode Exit fullscreen mode

By using the ConvertTypeTo type, we converted all properties of type string to number.
Another scenario might be that we want to include or exclude properties by type. Here we can write a building block type that can extract property keys based on a type.

type FilterByType<Type, ConvertibleType> = {
  [Key in keyof Required<Type>]: Required<Type>[Key] extends ConvertibleType ? Key : never;
}[keyof Type];
Enter fullscreen mode Exit fullscreen mode

Again, we iterate over all the keys for a given type and check if the key extends the type we want to filter on. Any key that does not extend the convertibleType is filtered out by returning never.
A short FilterByType test using our previously defined Box shows that we can retrieve all keys of type string.

// type BoxFilteredByTypeString = "id" | "content" | "color"
type BoxFilteredByTypeString = FilterByType<Box, string>;
Enter fullscreen mode Exit fullscreen mode

Now that we have our FilterByType in place we can write a custom type that either includes or excludes properties by type. To exclude we can use Omit again and combine it with our custom type:

type MakeExcludeByType<Type, ConvertibleType> = 
  Omit<Type, FilterByType<Type, ConvertibleType>>;
Enter fullscreen mode Exit fullscreen mode

To include all properties by type, we only need to replace Omit with Pick:

type MakeIncludeByType<Type, ConvertibleType> = 
  Pick<Type, FilterByType<Type, ConvertibleType>>;
Enter fullscreen mode Exit fullscreen mode

Here is an example showing how we can convert the Box type, by including or excluding all properties of type string.

/**
  type BoxOnlyVec2 = {
    size: Vec2;
    location: Vec2;
  }
 */
type BoxOnlyVec2 = MakeExcludeByType<Box, string>;

/**
  type BoxOnlyNumber = {
    id: string;
    content: string;
    color: string;
  }
 */
type BoxOnlyNumber = MakeIncludeByType<Box, string>;
Enter fullscreen mode Exit fullscreen mode

There are more transformations we can do, like for example making properties required, optional or read only based on a type or types. Here are more examples you can checkout

We should have a basic ideas of how to transform types now.


If you have any questions or feedback please leave a comment here or connect via Twitter: A. Sharif

Top comments (1)

Collapse
 
lohxx profile image
Lohanna Sarah

Very good article, it helped me to did the type challenges with typescript.