简体   繁体   中英

Infer type of function argument by other function argument in Typescript

I want to declare function type like

type ReviewChangeHandler = 
    | ((newReview: Review, oldReview: null, options: {type: 'CREATE'}) => void)
    | ((newReview: Review, oldReview: Review, options: {type: 'EDIT'}) => void)
    | ((newReview: null, oldReview: Review, options: {type: 'DELETE') => void)

I want the type of newReview and oldReview argument to be inferred by the value of options.type , so I can write code like:

switch (options.type) {
    case 'CREATE':
        // `newReview` inferred to type `Review` and `oldReview` inferred to type `null`
    case 'EDIT':
        // `newReview` inferred to type `Review` and `oldReview` inferred to type `Review`
    case 'DELETE:
        // `newReview` inferred to type `null` and `oldReview` inferred to type `Review`

How can I achieve such behavior?

There's a bunch going on here. First of all, I'm going to give Review a dummy definition so we can work with it:

interface Review {
  name: string;
}

Then, your union type of functions ReviewChangeHandler is unfortunately not going to be useful to you. You presumably don't want your handler to be able to handle just one of those parameter lists, right? You want it to handle all of those parameter lists. That is, you want a function of type ReviewChangeHandler to be the intersection of the three call signatures, not the union of them:

type ReviewChangeHandler =
  & ((newReview: Review, oldReview: null, options: { type: 'CREATE' }) => void)
  & ((newReview: Review, oldReview: Review, options: { type: 'EDIT' }) => void)
  & ((newReview: null, oldReview: Review, options: { type: 'DELETE' }) => void)

TypeScript considers the intersection of functions to be the same as a function with multiple call signatures; that is, it's an overloaded function. To write such a function you can declare multiple call signatures and then a single implementation signature that can accept any of the call signatures' inputs and return any of the call signatures' outputs. Like this:

// three call signatures
function reviewChangeHandler(newReview: Review, oldReview: null, options: { type: "CREATE" }): void;
function reviewChangeHandler(newReview: Review, oldReview: Review, options: { type: "EDIT" }): void;
function reviewChangeHandler(newReview: null, oldReview: Review, options: { type: "DELETE" }): void;

// one implementation signature
function reviewChangeHandler(newReview: Review | null, oldReview: Review | null, options: { type: "CREATE" } | { type: "EDIT" } | { type: "DELETE" }) {
  // impl here
}

You can double check that the above function declarations matches the new ReviewChangeHandler intersection type by assigning it to a variable of that type:

const check: ReviewChangeHandler = reviewChangeHandler; // okay

Now you would like the compiler to be able to infer narrower types for newReview and oldReview based on the type of options.type . Unfortunately this cannot really happen directly. The compiler will not understand that separate union-typed variables are correlated this way (see microsoft/TypeScript#30581 for discussion on the lack of support for correlated union types). Instead you might want to package your separate union-typed variables into a single object of a discriminated union type.

A plausible first attempt at this would be to just put newReview , oldReview , and options as properties of an object:

  const reviewChangeAttempt = {
    newReview,
    oldReview,
    options
  };

But you'd need to switch on reviewChangeAttempt.options.type , a nested discriminant property. And currently TypeScript does not recognize subproperties like this as valid discriminants. See microsoft/TypeScript#18758 for a feature request to support this.

If we want to gain the benefits of a discriminated union, we need to move the options.type subproperty up one level. So this gives us the following type:

type ReviewChangeObj = {
  newReview: Review;
  oldReview: null;
  type: "CREATE";
} | {
  newReview: Review;
  oldReview: Review;
  type: "EDIT";
} | {
  newReview: null;
  oldReview: Review;
  type: "DELETE";
}

and a possible implementation like this:

  const reviewChange = {
    newReview,
    oldReview,
    type: options.type
  } as ReviewChangeObj;

Note that I have to use a type assertion to convince the compiler that reviewChange is a valid ReviewChangeObj . The compiler's lack of ability to follow the correlation between newReview , oldReview , and options.type means that any repackaging of these into a type the compiler does understand would cause the compiler to object without something like a type assertion.

Also note that this throws away anything else that you might have put into options ; if you have other stuff in there you can hold onto it by making ReviewChangeObj elements have both an options and a type property. But I digress.


After this, finally, you get the inference you want:

function reviewChangeHandler(newReview: Review, oldReview: null, options: { type: "CREATE" }): void;
function reviewChangeHandler(newReview: Review, oldReview: Review, options: { type: "EDIT" }): void;
function reviewChangeHandler(newReview: null, oldReview: Review, options: { type: "DELETE" }): void;
function reviewChangeHandler(newReview: Review | null, oldReview: Review | null, options: { type: "CREATE" } | { type: "EDIT" } | { type: "DELETE" }) {

  const reviewChange = {
    newReview,
    oldReview,
    type: options.type
  } as ReviewChangeObj;

  switch (reviewChange.type) {
    case 'CREATE': {
      console.log("CREATING NEW REVIEW: " + reviewChange.newReview.name);
      break;
    }
    case 'EDIT': {
      console.log("EDITING OLD REVIEW: " + reviewChange.oldReview.name + " AND NEW REVIEW: " + reviewChange.newReview.name);
      break;
    }
    case 'DELETE': {
      console.log("DELETING OLD REVIEW: " + reviewChange.oldReview.name);
    }
  }
}

You can see that inside the case statements the compiler understands exactly when reviewChange.newReview and reviewChange.oldReview are going to be Review and when they are going to be null .


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