0

I have an issue writing proper typescript syntax for strict infering where:

  1. compiler properly reports on missing switch/case option
  2. returned value matches input kind by type
type KindA = {kind:'a'};
type KindB = {kind:'b'};
type KindC = {kind:'c'};
type AllKinds = KindA | KindB | KindC;

function create<T extends AllKinds>(kind:AllKinds['kind']):T {
  switch(kind) {
    case "a": return {kind:'a'};
    case "b": return {kind:'b'};
    case "c": return {kind:'c'};
  }
}

create("a");

Playground

I wonder if this is possible with the latest Typescript.

With my other approach (i.e. case "a": return {kind:'b'} as T;) the returned value is not type checked to what I need.

2 Answers 2

1

It is unsafe to return T in your case.

See why:

type KindA = { kind: 'a' };
type KindB = { kind: 'b' };
type KindC = { kind: 'c' };
type AllKinds = KindA | KindB | KindC;

function create<T extends AllKinds>(kind: AllKinds['kind']): T {
  switch (kind) {
    case "a": return { kind: 'a' };
    case "b": return { kind: 'b' };
    case "c": return { kind: 'c' };
  }
}

const result = create<{ kind: 'a', WAAAT: () => {} }>("a")
result.WAAAT() // compiler but causes an error in runtime

Generic argument should in 90% of cases depend on input value.

See this example:

type KindA = { kind: 'a' };
type KindB = { kind: 'b' };
type KindC = { kind: 'c' };
type AllKinds = KindA | KindB | KindC;


const builder = <Kind extends AllKinds['kind']>(kind: Kind) => ({ kind })

const result = builder("a"); // {kind: 'a' }

Playground

See this answer, this answer and my article for more context

Do I get it right that witch current Typescript, one can not have both ?

The problem is not in switch statement but rather in explicit return type. Return type can't depend on a generic value which is not binded with function arguments.

In fact, it is possible to achieve waht you want:

type KindA = { kind: 'a' };
type KindB = { kind: 'b' };
type KindC = { kind: 'c' };
type AllKinds = KindA | KindB | KindC;

function create<Kind extends AllKinds['kind']>(kind: Kind): Extract<AllKinds, { kind: Kind }>
function create(kind: AllKinds['kind']) {
    switch (kind) {
        case "a": return { kind: 'a' };
        case "b": return { kind: 'b' };
        case "c": return { kind: 'c' };
    }
}

const result1 = create("a") // KindA
const result2 = create("b") // KindB
const result3 = create("c") // KindC

Playground

As you might have noticed, I have used function overloading. It makes TS compiler less strict. In other words, provides a bit of unsafety but in the same time makes it more readable and infers return type.

AFAIK, function overloads behaves bivariantly. Hence, it is up to you which option is better

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks for the quick answer. Do I get it right that witch current Typescript, one can not have both: 1. switch/case for kind at the same time as 2. return type check? Or can somehow both points from my original question matched?
@Yoz made an update
this version would let me do case "a": return { kind: 'b' };
This is why it is unsafe approach. Just like I said
Thanks, got, it... I will keep it open to see if anyone can come up with an idea to match both 1. and 2.
1

The error is quite descriptive.

Type '{ kind: "a"; }' is not assignable to type 'T'.
  '{ kind: "a"; }' is assignable to the constraint of type 'T',
  but 'T' could be instantiated with a different subtype of constraint 'AllKinds'.

This problem occurs even when you use a simpler type constraint.

function foo<T extends string>(): T {
  return 'foo';
}

Here we'd get the following error.

Type 'string' is not assignable to type 'T'.
  'string' is assignable to the constraint of type 'T',
  but 'T' could be instantiated with a different subtype of constraint 'string'.

The problem is that we said that we'd return something of type T, but T is not the same type as string. Yes, the type T extends string but all that means is that T is a subtype of string. For example, the type 'bar' is a subtype of string. Hence, we can instantiate T with 'bar'. Hence, we'd expect the return value to be 'bar' but the return value is 'foo'.

The solution is to simply not use generics. If you want to return a string then just say that you're returning a string. Don't say that you're returning a value of some subtype T of string.

function foo(): string {
  return 'foo';
}

Similarly, if you want to return a value of type AllKinds then just say that you're returning a value of type AllKinds. Don't say that you're returning a value of some subtype T of AllKinds.

type KindA = {kind:'a'};
type KindB = {kind:'b'};
type KindC = {kind:'c'};
type AllKinds = KindA | KindB | KindC;

function create(kind:AllKinds['kind']): AllKinds {
  switch(kind) {
    case "a": return {kind:'a'};
    case "b": return {kind:'b'};
    case "c": return {kind:'c'};
  }
}

create("a");

Edit: You need dependent types to do what you want. TypeScript doesn't have dependent types. However, you can create a custom fold function that provides additional type safety.

type Kind = 'a' | 'b' | 'c';

type KindA = { kind: 'a' };
type KindB = { kind: 'b' };
type KindC = { kind: 'c' };

type AllKinds = KindA | KindB | KindC;

function foldKind<A, B, C>(a: A, b: B, c: C): (kind: Kind) => A | B | C {
  return function (kind: Kind): A | B | C {
    switch (kind) {
      case 'a': return a;
      case 'b': return b;
      case 'c': return c;
    }
  }
}

const create: (kind: Kind) => AllKinds = foldKind<KindA, KindB, KindC>(
  { kind: 'a' },
  { kind: 'b' },
  { kind: 'c' }
);

Now, you can only provide a value of KindA for 'a', a value of KindB for 'b', and a value of KindC for 'c'. See the demo for yourself.

5 Comments

Thanks for the quick answer. Do I get it right that witch current Typescript, one can not have both: 1. switch/case for kind at the same time as 2. return type check? Or can somehow both points from my original question matched?
You can have both, as is shown in my answer above. I don't know why you're trying to shoehorn in a generic type though.
with the proposed syntax, one can do case "a": return {kind:'b'}; - means compiler is not guarding as I would like it to. (see my point 2)
You need dependent types to do what you want. TypeScript doesn't have dependent types. However, you can create a custom fold function that provides additional type safety. See my answer for more details.
Thanks for looking into it Aadit. Still seem not generic enough (Imagine I have 10+, 100+ kinds) and also create("a") returns AllKind instead of KindA. I understand this is a current limitation of the language so I have created a feature request github.com/microsoft/TypeScript/issues/46236 .

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.