Reputation: 457
I have the following TS code with multiple interfaces, components and a key/interface map.
interface FooProps {
'keyInFoo': string
}
const Foo = (props: FooProps) => {}
interface BarProps {
'keyInBar': string
}
const Bar = (props: BarProps) => {}
interface PropsMap {
'foo': FooProps,
'bar': BarProps,
//.. baz, etc...
}
type keyName = keyof PropsMap
const MyFunc = <T extends keyName>(key: T) => {
const props: PropsMap[T] = getProps(key)
switch(key) {
// We should know since T == typeof key === 'foo'
// that typeof props === FooProps
case 'foo': return Foo(props) // <- ERROR: Property ''keyInFoo'' is missing in type 'BarProps'
case 'bar': return Bar(props) // <- ERROR Property ''keyInBar'' is missing in type 'FooProps'
}
}
const getProps = <T extends keyName>(key: T): PropsMap[T] => {
// ... some logic to get the props
return {} as PropsMap[T]
}
I want to run a switch/if-else statement against the key of the map, and have TypeScript know the value of props
in each case.
Of course, I could just assert props as FooProps
etc. in each case, but I feel like there should be a way to have TS infer the type.
I've also tried using a generic type as a map with no additional success:
type PropsGeneric<T extends keyName> =
T extends 'foo' ? FooProps :
T extends 'bar' ? BarProps : never;
See TS Playground
A working, but verbose solution I've come up with is to create a type guard for each key, and use this in a series of if-else if statements within MyFunc
const isFoo = <T extends keyName>(key: T, props: PropsMap[T]): props is FooProps => key === 'foo'
const isBar = <T extends keyName>(key: T, props: PropsMap[T]): props is BarProps => key === 'bar'
// ... etc
Upvotes: 2
Views: 231
Reputation: 23835
As you already noticed, it is tricky to make TypeScript understand the typings of functions implementations when generics are involved. Narrowing the type of props
by checking the value of key
when both are generic types does not work. Both types stay untouched by the compiler.
The first thing I would change is the PropsMap
. Let's use the types of the functions as the value types so we can use the ReturnType
and Parameters
utility types later.
interface PropsMap {
'foo': typeof Foo,
'bar': typeof Bar
}
type keyName = keyof PropsMap
const getProps = <T extends keyName>(key: T): Parameters<PropsMap[T]>[0] => {
// ... some logic to get the props
return {} as Parameters<PropsMap[T]>[0]
}
There are a few ways to write conditionals, so that TypeScript properly understands them. They mostly resemble some type of lookup object which can be strongly typed.
const MyFunc = <T extends keyName>(key: T) => {
const props = getProps(key)
const ret: {
[K in keyName]: (arg: Parameters<PropsMap[K]>[0]) => ReturnType<PropsMap[K]>
}[T] = {
foo(props: FooProps) {
return Foo(props)
},
bar(props: BarProps) {
return Bar(props)
}
}[key]
return ret(props)
}
Our lookup-object called ret
is typed with a mapped type. For each key in PropsMap
, ret
also has a corresponding key which holds a method. Each method takes props
as its parameter. The props
parameter and the return type of the methods are strictly typed by the mapped type.
All that's left to do is to call ret
with the props
.
This leaves us with an implementation that frankly is quite verbose and also has some runtime overhead due to the added function call. But it is a way to strongly type the implementation without type assertions.
Upvotes: 1