In this post we will see how we can use Typescript typing system to create a Mapper helper.
Let's imagine we have an object like this one :
interface IGraphicControl {
width : number;
height : number;
alpha : number;
fillColor : string | number;
drawRect(x: number, y: number, width: number, height: number): void;
render(): void;
}
Now if we need to set several properties we need to do the following.
const myGraphic = new Graphic();
myGraphic.width = 100;
myGraphic.height = 100;
myGraphic.alpha = 1;
myGraphic.fillColor = 0x00FF00;
myGraphic.drawRect(0,0,50,50);
myGraphic.fillColor = 0x0000FF;
myGraphic.drawRect(50,50,50,50);
myGraphic.render()
We want to simplify the mapping a little bit so we can do this :
setTo(myGraphic, {
width : 100,
height : 100,
alpha : 1,
fillColor : 0x00FF00,
drawRect : [0,0,50,50] // Call the function
})
We want to be able to define all properties with the correct values, and call functions with parameters as tuples. But we want that for every object we pass as the first parameter, the second parameter provides the right intellisense.
To create such a function, we will have to extract all the information from the first parameter.
We will need to extract all properties and functions, and treat the functions as tuples of parameters, correctly typed.
Step 1
Create a type that will invalidate properties that do not correspond to the type you are looking for.
type ConditionalTypes<Base, Condition> = {
[Key in keyof Base]: Base[Key] extends Condition ? never : Key;
}
So we create a type in the form of a hashMap whose keys are the properties of the Base type, and whose type will be either a string of the name of the key, or an impossibility of assignment.
type newFilteredType = ConditionalTypes<IGraphicControl, Function>;
// Will be the same as
type newFilteredType = {
width : "width";
height : "height";
alpha : "alpha";
fillColor : "fillColor";
drawRect : never;
render : never;
}
So why create a type whose properties are string values ?
Simply because now we can extract those types.
Step 2
We need to extract the valid keys, but it is not possible to list the keys we want to keep. Instead we can extract all property types of a type, excluding those of never type.
// We will change this
type ConditionalTypes<Base, Condition> = {
[Key in keyof Base]: Base[Key] extends Condition ? never : Key;
}
// to
type ConditionalTypes<Base, Condition> = {
[Key in keyof Base]: Base[Key] extends Condition ? never : Key;
}[keyof Base]
Now we can retreive all types excluding nerver types. The tricky part comes here, as each valid type is a string :-). We will retreive all valid names as string.
type newFilteredType = ConditionalTypes<IGraphicControl, Function>;
// Will be the same as
type newFilteredType = "width" | "height" | "alpha" | "fillcolor";
Step 3
Now we need to extract the real types of the selected keys.
We will use the Pick type.
// We will change this
type ConditionalTypes<Base, Condition> = {
[Key in keyof Base]: Base[Key] extends Condition ? never : Key;
}[keyof Base]
// to
type ConditionalTypes<Base, Condition> = Pick<Base, {
[Key in keyof Base]: Base[Key] extends Condition ? never : Key;
}[keyof Base]>
And then this will result in the following
type newFilteredType = ConditionalTypes<IGraphicControl, Function>;
// Will be the same as
type newFilteredType = {
width : number;
height : number;
alpha : number;
fillColor : number | string;
}
Yessssss, we got it !!!
Step 4
We need now to get all fields that are not functions, and all that are functions to process them differently.
So let's change our type again
type ConditionalTypes<Base, Condition> = Pick<Base, {
[Key in keyof Base]: Base[Key] extends Condition ? never : Key;
}[keyof Base]>
// to
type ConditionalTypes<Base, Condition, Extract extends Boolean> = Pick<Base, {
[Key in keyof Base]: Extract extends true ?
Base[Key] extends Condition ? Key : never
:
Base[Key] extends Condition ? never : Key
}[keyof Base]>;
We added an third type that extends boolean, so we will use it to define if we want to extract selected type, or exclude it.
Now we are able to get what we want.
type newFilteredType = ConditionalTypes<IGraphicControl, Function, false>;
// Will be the same as
type newFilteredType = {
width : number;
height : number;
alpha : number;
fillColor : string | number;
}
// AND
type newFilteredType = ConditionalTypes<IGraphicControl, Function, true>;
// Will be the same as
type newFilteredType = {
drawRect(x: number, y: number, width: number, height: number): void;
render(): void;
}
Step 5
We are now able to separate properties into two categories, functions and the remainder.
We need to rebuild a type whose functions will no longer be defined as functions, but as an array of typed parameters.
We will use the Parameters type, that can extract all parameter types and put them in a tuple.
type ParameterType<T> = Partial<
ConditionalTypes<T, Function, false> // Properties that are not functions
&
{
[K in keyof ConditionalTypes<T, Function, true>]: Parameters<ConditionalTypes<T, Function, true>[K]> // Tuple
}
>;
Step 6
The target prototype is
function setTo<T>(source: T, value: ParameterType<T>): void
And to use it
setTo(myGraphic, {
width : 100,
height : 100,
alpha : 1,
fillColor : 0x00FF00
});
setTo(myGraphic, {
drawRect: [0,0,50,50]
}
setTo(myGraphic, {
render: []
}
We still need to do an extra call to render after because the render should not be called at the same time, but after. So it is not very usefull as is.
Final step
As a bonus, we will add a way to Chain several call without the need to pass the source as a parameter
function setTo<T>(source: T, value: ParameterType<T>) {
for(const key in value) {
if (key in source) {
typeof source[key as keyof T] === "function" ?
(source[key as keyof T] as unknown as Function).apply(source, (value as unknown as any)[key])
:
source[key as keyof T] = (value as unknown as any)[key];
}
}
return (nextValue: ParameterType<T>) => setTo(source, nextValue);
}
We did it !
As a result, we can now do the following
setTo(myGraphic, {
width : 100,
height : 100,
alpha : 1,
fillColor : 0x00FF00
})({
drawRect : [0,0,50,50]
})({
alpha : 0.5,
fillColor : 0xFFFF00,
})({
drawRect : [50,50,50,50]
})({
render: [];
})
For big declaration like animations, this can reduce the amount of code. This sample may not be the most accurate but it shows you how much powerful typescript can be.
On a day to day basis, you don't need to deal with advanced typing, but if you create helpers in libraries or frameworks, you can provide a very usefull intellisense and type constraint that will save developers a lot of time and debugging hours..
Enjoy !
Top comments (0)