Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
274 views
in Technique[技术] by (71.8m points)

reactjs - 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?

question from:https://stackoverflow.com/questions/65886854/can-i-use-conditional-generic-to-set-a-callback-return-type-in-typescript

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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#13995 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>:

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


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...