简体   繁体   中英

Typescript switch map

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

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.


Playground

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM