Reputation: 1699
I have a type that associates names to types by mean of a Mapping Object Type TMap
it aims to feed a handler function with an association of one of the named types with a correspondent typed value
interface Thing<TMap extends { [k: string]: any }> {
get<T extends keyof TMap>(handler: (v: TMap[T], t: T) => unknown): unknown
}
for example, say the Mapping Object type to be:
interface Types {
num: number,
dat: Date,
str: string
}
and an instance of Things:
declare const thing: Thing<Types>
using the get
method to get a couple of value, type works but loses type association when checking the type:
thing.get((v, t) => {
// v: string | number | Date
// t: "num" | "dat" | "str"
if (t === 'num') {
// v: string | number | Date
v
} else if (t === 'dat') {
// v: string | number | Date
v
} else if (t === 'str') {
// v: string | number | Date
v
}
})
I managed a tricky workaround to fix that:
type Caster<TMap extends { [k: string]: any }> =
<T extends keyof TMap>(
v: TMap[keyof TMap],
t: keyof TMap,
isOfType: T
) => v is TMap[T]
declare const caster: Caster<Types>
thing.get((v, t) => {
// v: string | number | Date
// t: "num" | "dat" | "str"
if (caster(v, t, 'num')) {
// v: number
v
} else if (caster(v, t, 'dat')) {
// v: Date
v
} else if (caster(v, t, 'str')) {
// v: string
v
}
})
How to properly declare Thing
and Thing.get
to keep type association avoiding tricky hacks?
[edit] check it out on TSPlayground
Upvotes: 3
Views: 1288
Reputation: 249936
Type guards don't support narrowing the type of a different value then the one checked.
What we can do is change the callback to take in an object that is a union where a member of a union has the form { type: P, value T[P] }
where T
is the map type and P
in turn every property of T
// Type that creates a union using the distributive property of union types.
type TypeUnion<T> = keyof T extends infer P ? // Introduce an extra type parameter P to distribute over
P extends any ? { type: P, value: T[P] } : // Take each P and create the union member
never : never;
interface Thing<TMap extends { [k: string]: any }> {
get(handler: (v: TypeUnion<TMap>) => unknown): unknown
}
declare const thing: Thing<Types>
interface Types {
num: number,
dat: Date,
str: string
}
thing.get(o => { // o is { type: "num"; value: number; } | { type: "dat"; value: Date; } | { type: "str"; value: string; }
if (o.type === 'num') {
o.value // number
} else if (o.type === 'dat') {
o.value // Date
} else if (o.type === 'str') {
o.value // string
}
})
An alternative to conditional types for TypeUnion
would be to use mapped type like this:
type TypeUnion<T> = {
[P in keyof T] : { type: P, value: T[P]}
}[keyof T]
Upvotes: 3