简体   繁体   中英

How to specialize type of discriminated union?

Assume I have two discriminated unions and a third type alias which is a union type containing both.

How do I create a function which can take both the discriminated unions but whose return type depends on the specific type of the value passed?

I thought this would work, given that Specialize would return { value: number } if the passed value belongs to First and number if it belongs to Second , but it doesn't.

type First = "a" | "b";
type Second = "c" | "d";
type Letter = First | Second;
type Specialize<R> = R extends First ? { value: number } : number;

const fun = <M extends Letter, R extends Specialize<M>>(letter: M): R => {
    return letter === "a" || letter === "b"
        ? { value: 1 }
        : 2;
}

What am I misunderstanding here?

Please notice that I do not want to remove the R which constrains the return type. Instead, I'd like to constrain the return type to the passed argument.


Type '{ value: number; } | 2' is not assignable to type 'R'.
  '{ value: number; } | 2' is assignable to the constraint of type 'R', but 'R' could be instantiated with a different subtype of constraint 'number | { value: number; }'.
    Type '{ value: number; }' is not assignable to type 'R'.
      '{ value: number; }' is assignable to the constraint of type 'R', but 'R' could be instantiated with a different subtype of constraint 'number | { value: number; }'.

This is a job for overloads. By providing specific overloads for narrowed cases, the return value will also be narrowed.

// Overloads
function fun(letter: First): { value: number };
function fun(letter: Second): number;
// General case
function fun(letter: Letter): { value: number } | number {
    return letter === "a" || letter === "b"
        ? { value: 1 }
        : 2;
}

const x = fun("a")   // x: { value: number }
const y = fun("c")   // y: number

Your generics-based approach can't work because the caller gets to decide what R is, and R can be any type that extends Specialize<M> . That's what the errors are telling you. You have no way to return the specific subtype the caller has chosen.

(Given TypeScript's structural typing, I'm not aware of particular way to create a subtype of Specialize<R> . But the compiler can't prove that it's impossible, and I'm sure someone more clever with TypeScript than I am can think of one. But by using extends you specifically said that the caller can require a subtype, and that makes returning R impossible.)

The main problem is the polymorphism given by the generic constraint.

Defining your function as generic with an argument type which extends from another, you must ensure your return statement actually returns a value which is assignable of type T and its extensions (where T is the type argument).

Let's suppose you define this type

type MoreSpecialize<R> = R extends First ? { value: 10 } : 20;

... which extends from Specialize<> . I could invoke the function fun with this type argument,

const r: MoreSpecialize<Letter> = fun2<Letter, MoreSpecialize<Letter>>('a');

But, r is not actually of type MoreSpecialize<Letter> because fun returns { value: 1 } | 2 { value: 1 } | 2 , not { value: 10 } | 20 { value: 10 } | 20 . That's why the compiler complains.

Solution

  1. Define your function with concrete types if you are not going to need extensions, indeed.
const fun = (letter: Letter): Specialize<Letter> => {
    return letter === "a" || letter === "b"
        ? { value: 1 }
        : 2;
}
  1. Force casting the return value [.].
    Not the best way to go.
const fun2 = <M extends Letter, R extends Specialize<M>>(letter: M): R => {
    return (
        letter === "a" || letter === "b"
            ? { value: 1 }
            : 2
    ) as R;
}
  1. @RobNapier's solution is totally fine.

Hope it helps.

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