简体   繁体   中英

Filter on keyof type parameter in typescript

No need to know React, just some background: in my use case, I want to amplify user specified React.ComponentClass with my custom Higher Order Component. In order to do so, I want user to also send me list of names of the particular props my Higher Order Component will inject. That would be done like this:

function listToComponentProps<TComponentProps, TKey = keyof TComponentProps>(props: Array<TKey>) {
  // Logic here, not important for the example
}
interface TestProps {a: number, b: Function, c: () => number}
listToComponentProps<TestProps>(['a', 'b', 'c'])

The keyof keyword handles the constraint for me. Example inputs for listToComponentProps<TestProps> would be

  • valid: ['b'] , ['b', 'c'] , ['c']
  • invalid
    • ['a'] , ['a', 'b'] , ['a', 'b', 'c'] (a is number, not a function)
    • ['d'] , ['d', 'c'] (d is not part of the interface TestProps

The problem is, I want to restrict the props parameter not only to be key of TComponentProps , but also such key that the corresponding type in TComponentProps is Function (so that 'a' would be an invalid option detected by typescript compiler). How can one achieve such task?

You could do it like this:

const listToComponentProps = <
  TComponent extends {[P in TKey]: Function },
  TKey extends string = keyof TComponent>
  (props: TKey[]) => { /* ... */ };

interface TestProps {a: number, b: Function, c: () => number}
const result = listToComponentProps<TestProps>(['a', 'b', 'c']) // Type error

This results in the type error:

Type 'TestProps' does not satisfy the constraint '{ a: Function; b: Function; c: Function; }'.
  Types of property 'a' are incompatible.
    Type 'number' is not assignable to type 'Function'.

Unfortunately this business of having a default parameter ends up constraining our TComponent to have only Function properties. When you genuinely want to pass something like listToComponentProps<TestProps>(['b', 'c']) , which should be valid, you will need to explicitly fill in the second type parameter, ie listToComponentProps<TestProps, 'b' | 'c'>(['b', 'c']) listToComponentProps<TestProps, 'b' | 'c'>(['b', 'c']) .

What you really want is not to have a default parameter for TKey , but rather for generic inference to be granular: in a type parameter list, all parameters that can be inferred (eg TKey , which can be inferred from the passed array) should be inferred, even if some (in this case, TComponent ) have to be specified manually. TypeScript doesn't work this way today, so we're SOL.

There's a bunch of open issues on the TypeScript issue tracker about this, you could go find them and kick up a stink.


If good inference is more important to you than strictly preserving runtime characteristics, you could add a dummy argument to dissociate inference for the two type parameters:

const listToComponentProps =
    <TKey extends string>
    (props: TKey[]) =>
    <TComponent extends {[P in TKey]: Function }>
    () => { /* ... */ };

interface TestProps { a: number, b: Function, c: () => number }

const result = listToComponentProps(['a', 'b', 'c'])<TestProps>() // Type error
const result2 = listToComponentProps(['b', 'c'])<TestProps>() // OK

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