Issue
How can I narrow down the class that will be instanciated using mapped types like this ?
export enum WidgetType {
PIE = "pie",
BAR = "bar",
COUNTER = "counter",
}
class CounterHandler{}
class GroupValuesHandler{}
const HANDLERS_MAP = {
[WidgetType.COUNTER]: CounterHandler,
[WidgetType.BAR]: GroupValuesHandler,
[WidgetType.PIE]: GroupValuesHandler,
} as const;
export type GetHandler<T extends WidgetType> = {
[K in WidgetType]: typeof HANDLERS_MAP[K];
}[T];
type B = GetHandler<WidgetType.COUNTER>; // CounterHandler -> OK
export class WidgetHandlerMapper {
public getHandler<T extends WidgetType>(
key: WidgetType,
): InstanceType<GetHandler<T>> {
return new HANDLERS_MAP[key](); // Error, narrows to CounterHandler | GroupValuesHandler
}
}
const mapper = new WidgetHandlerMapper()
const b = mapper.getHandler(WidgetType.COUNTER) // typeof b should be CounterHandler
Solution
Firstly your generic is setup incorrectly:
export class WidgetHandlerMapper {
public getHandler<T extends WidgetType>(
key: T, // <-- change here so we can infer parameter
): InstanceType<GetHandler<T>> {
return new HANDLERS_MAP[key](); // Error, narrows to CounterHandler | GroupValuesHandler
}
}
But even then we still get an error, this seems to be a bug (at first), because if you instantiate the new class outside of getHandler instead of inside it works fine
export class WidgetHandlerMapperWithoutClassInstantiation {
public getHandler<T extends keyof typeof HANDLERS_MAP>(
key: T,
) {
return HANDLERS_MAP[key];
}
}
const mapper1 = new WidgetHandlerMapperWithoutClassInstantiation()
const demo1 = new (mapper1.getHandler(WidgetType.COUNTER))();
// ^? = const demo1: CounterHandler
When the object is then instantiated inside your generic function, it has to parse it then and there, before we even know the type of T
, and it does it's best, sees T extends WidgetType
. While we can assume safely the type of T
, the control flow analysis in TS only stays within it's own scopes. The new
keyword creates a different scope. When it gets instantiated into it's own scope, it loses context of the value of T
but knows it extends WidgetType
.
/**
* An analgous demonstration of scopes
*/
function functionInFunction<
F extends (() => string) |
(() => number)
>(func: F) {
return func()
//instead you have to cast it
//return func() as ReturnType<F>
}
const test2 = functionInFunction(() => '')
// ^? const test2: string | number
// huh??
The solution is to cast the return value, effectively asserting that the inner scope is dependent on the outer scope, and you know how it relates (in this case it is InstanceType<GetHandler<T>>
).
export class WidgetHandlerCast {
public getHandler<T extends WidgetType>(
key: T
): InstanceType<GetHandler<T>> {
return new HANDLERS_MAP[key]() as InstanceType<GetHandler<T>>;
}
}
Answered By - Cody Duong
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.