I have a polymorficFactory function, that will instantiate classes based on the 'discriminator' property provided:
type ClassConstructor<T> = {
new (...args: any[]): T;
};
type ClassMap<T> = Record<string, ClassConstructor<T>>;
function polymorficFactory<T extends object>(
classMap: ClassMap<T>,
discriminator: string,
input: Record<string, any>,
): T {
if (!input[discriminator]) throw new Error('Input does not have a discriminator property');
const discriminatorValue = input[discriminator];
const constructor = classMap[discriminatorValue];
return plainToInstance(constructor, input); // class-transformer util
}
It's used on unknown objects coming from request payloads:
const MOCK_PAYLOAD = {
type: 'SOME_DTO',
someProperty: 1,
someStr: 'Lorem',
someNestedProp: {
someBool: true,
}
} as as Record<string, unknown>; // Record<string, unknown/any>, since comes from JSON payload, but will always be an object
const dto = polymorficFactory<SomeDto | SomeOtherDto>(
{
SOME_DTO: SomeDto,
SOME_OTHER_DTO: SomeOtherDto,
},
'type',
MOCK_PAYLOAD ,
);
dto; // when hovering, type will be "SomeDto | SomeOtherDto"
If I wont provide the type parameters:
const dto = polymorficFactory(
...
);
dto; // when hovering, type will be "SomeDto"
It selects the first value it finds in this object:
{
SOME_DTO: SomeDto,
SOME_OTHER_DTO: SomeOtherDto,
}
Is there a way that I can make ts infer the union type, SomeDto | SomeOtherDto
or any other Dto classes found in the provided classMap, from the map values, without explicitly providing it?
I.e., for the following classMap:
{
A: aDto,
B: bDto,
C: cDto,
}
I'd expect the following union type inferred for the result:
aDto | bDto | cDto
CodePudding user response:
You need to play with inference on function arguments a little bit. The rule of thumb: If you want to infer a value, you should replay all transformations which are done in a runtime in type scope.
I mean if you build a class instance, you need to call InstanceType
built in type in order to get class instance type.
These class maps worth making immutable or in other words use as const
assertion.
COnsider this:
import { plainToInstance } from "class-transformer";
type ClassConstructor<T> = {
new(...args: any[]): T;
};
function polymorficFactory<
Inst,
Values extends PropertyKey,
Discriminator extends string,
ClassMap extends Record<Values, ClassConstructor<Inst>>,
Input extends Record<Discriminator, Values>
>(
classMap: ClassMap,
discriminator: Discriminator,
input: Input,
): InstanceType<ClassMap[Input[Discriminator]]>
function polymorficFactory<
Inst,
Values extends PropertyKey,
Discriminator extends string,
ClassMap extends Record<Values, ClassConstructor<Inst>>,
Input extends Record<Discriminator, Values>
>(
classMap: ClassMap,
discriminator: Discriminator,
input: Input,
) {
if (!input) throw new Error('Input is not an object');
if (!input[discriminator]) throw new Error('Input does not have a discriminator property');
const discriminatorValue = input[discriminator];
const klass = classMap[discriminatorValue];
const result = plainToInstance(klass, input)
return result
}
class SomeDto {
tag = 'SomeDto'
}
class SomeOtherDto {
tag = 'SomeOtherDto'
}
const CLASS_MAP = {
SOME_DTO: SomeDto,
SOME_OTHER_DTO: SomeOtherDto,
} as const
const MOCK = {
type: 'SOME_OTHER_DTO',
someProperty: 1,
} as const
const explicitParams = polymorficFactory(
CLASS_MAP,
'type',
MOCK,
);
explicitParams; // when hovering, type will be " SomeOtherDto"
As you might have noticed each value in polymorficFactory
is infered properly.
- See this line
const discriminatorValue = input[discriminator];
. If you expectdiscriminator
to be a valid index forinput
, you should apply appropriate constraint:
function polymorficFactory<
// .....
Values extends PropertyKey,
Discriminator extends string,
Input extends Record<Discriminator, Values>
>(
// ...
discriminator: Discriminator,
input: Input,
)
input
variable should extend Record<Discriminator, Values>
. It means that discriminator
is allowed index for input
.
Consider this line const klass = classMap[discriminatorValue];
discriminatorValue
which is input[discriminator]
should be a valid key for classMap
, hence, we need to apply here an appropriate constraint:
function polymorficFactory<
Inst,
Values extends PropertyKey,
Discriminator extends string,
ClassMap extends Record<Values, ClassConstructor<Inst>>,
Input extends Record<Discriminator, Values>
>(
classMap: ClassMap,
discriminator: Discriminator,
input: Input,
)
See Values
generic, it plays a role of value
in Input
and a key in ClassMap
. It is important.
So now, when all arguments are infered, we need to infer a return type. Our return type is an instance of classMap[input[discriminator]]
, or in type
words: InstanceType<ClassMap[Input[Discriminator]]>
If you are interested in more explanation of inference on function arguments, you can check my article