简体   繁体   中英

Can I use conditional generic to set a callback return type in typescript?

I have a react component to which I'm passing a generic. Based on this generic I would like to change the callback payload type.

In the example below I'm passing the RelationType which can be 'one' or 'many' and based on this the callback should be either a string or array of strings.

import React from 'react';

export enum RelationType {
  one = 'one',
  many = 'many',
}

interface Props<R extends RelationType> {
  callback(newValue?: R extends RelationType.one ? string : string[]): void;
  relationType: R;
}

export const Component = <R extends RelationType>({ onUpdate, relationType }: Props<R>) => {
  return (
    <button onClick={() => {
      if (relationType === RelationType.one) {
        callback('foo'); // TS2345: Argument of type '"foo"' is not assignable to parameter of type '(R extends RelationType.one ? string : string[]) | undefined'.
      } else {
        callback(['foo', 'bar']); // TS2345: Argument of type 'string[]' is not assignable to parameter of type 'R extends RelationType.one ? string : string[]'.
      }
    }}/>
  )
};

Is this possible with Typescript?

The generic type parameter R in the Props type and the Component function is not helping you. Inside of Component() you are trying to check relationType to narrow callback , but generic type parameters like R are never narrowed via control flow analysis in TypeScript. See microsoft/TypeScript#24085 for more information.

Instead it would be best to just use a non-generic Props of a discriminated union type, equivalent to your original Props<RelationType.one> | Props<RelationType.many> Props<RelationType.one> | Props<RelationType.many> :

type Props = { [R in RelationType]: {
  callback(newValue?: R extends RelationType.one ? string : string[]): void;
  relationType: R;
} }[RelationType];

/* type Props = {
    callback(newValue?: string | undefined): void;
    relationType: RelationType.one;
} | {
    callback(newValue?: string[] | undefined): void;
    relationType: RelationType.many;
} */

In the above I've had the compiler calculate that union programmatically by mapping your original Props<R> definition over RelationType to form an object type whose properties I immediately look up .


Then your Component() function can use a parameter of type Props . Another caveat is that you cannot destructure it into relationType and callback inside the implementation signature if you want the compiler to keep track of the relationship between the two values. TypeScript doesn't have support for what I've been calling correlated union types (see microsoft/TypeScript#30581 ). You need to keep both values as properties of a single props parameter if you want the behavior you're looking for:

const Component = (props: Props) => {
  return (
    <button onClick={() => {
      if (props.relationType === RelationType.one) {
        props.callback('foo');
      } else {
        props.callback(['foo', 'bar']);
      }
    }} />
  )
};

That works as desired, I think.

Playground link to code

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