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;
};
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'.
*/
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>;
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}
});
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];
};
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>;
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];
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>;
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>>;
To include all properties by type, we only need to replace Omit with Pick:
type MakeIncludeByType<Type, ConvertibleType> =
Pick<Type, FilterByType<Type, ConvertibleType>>;
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>;
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
Oldest comments (1)
Very good article, it helped me to did the type challenges with typescript.