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
Top comments (1)
Very good article, it helped me to did the type challenges with typescript.