Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow switch type guards #2214

Closed
icholy opened this issue Mar 5, 2015 · 54 comments
Closed

Allow switch type guards #2214

icholy opened this issue Mar 5, 2015 · 54 comments
Labels
Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Help Wanted You can do this Suggestion An idea for TypeScript

Comments

@icholy
Copy link

icholy commented Mar 5, 2015

I have the following code:

function logNumber(v: number) { console.log("number:", v); }
function logString(v: string) { console.log("string:", v); }

function foo1(v: number|string) {
    switch (typeof v) {
        case 'number':
            logNumber(v);
            break;
        case 'string':
            logString(v);
            break;
        default:
            throw new Error("unsupported type");
    }
}

Error:

Argument of type 'string | number' is not assignable to parameter of type 'number'.
 Type 'string' is not assignable to type 'number'.

I was forced to rewrite this using if statements.

function foo2(v: number|string) {
    if (typeof v === 'number') {
        logNumber(v);
    } else if (typeof v === 'string') {
        logString(v);
    } else {
        throw new Error("unsupported type");
    }
}

Please allow using switch statements as type guards.

@ghost
Copy link

ghost commented Mar 21, 2015

Yes please allow switch case there for state-machine. Also the conditional/ternary operator (?:).

@zspitz
Copy link
Contributor

zspitz commented Apr 15, 2015

+1 for switch and ?:
@RyanCavanaugh @jasonwilliams200OK Isn't this the same as #2388 ?
@icholy In the meantime, you could use explicit type casting:

switch (typeof v) {
    case 'number':
        logNumber(<number>v);
        break;
    case 'string':
        logString(<string>v);
        break;
    default:
        throw new Error("unsupported type");
}```

@RyanCavanaugh RyanCavanaugh added Help Wanted You can do this and removed In Discussion Not yet reached consensus labels May 4, 2015
@RyanCavanaugh RyanCavanaugh added this to the Community milestone May 4, 2015
@RyanCavanaugh
Copy link
Member

Approved

@AbraaoAlves
Copy link

+1

@jednano
Copy link
Contributor

jednano commented Jan 23, 2016

Is this the same as #2388?

@DanielRosenwasser
Copy link
Member

Not quite the same thing, since you don't need #2388 for this.

@goodmind
Copy link

goodmind commented Aug 6, 2016

Is this allow user-defined type guards in switch cases?

Like this:

interface MyType1 { type: number }
interface MyType2 { test: string }

function isType1 (x): x is MyType1 {
   return !!x.type && typeof x.type === 'number'
}

function isType2 (x): x is MyType2 {
  return !!x.test && typeof x.test === 'string'
}

let x = getType1OrType2()

switch (true) {
   case isType1(x):
     console.log('x is MyType1')
     break;
   case isType2(x):
     console.log('x is MyType2')
     break;
  default:
     console.log('Unknown type')
}

@electricessence
Copy link

@goodmind Your above suggestion breaks common switch rules. Cases have to be constants.

@goodmind
Copy link

@electricessence ok. i think #165 better for something like my example (but without switch, maybe auto-generated user type guards or so)

@icholy
Copy link
Author

icholy commented Nov 15, 2016

@electricessence what do you mean by "common switch rules"? The example provided by @goodmind is valid code.

@rob3c
Copy link

rob3c commented Nov 16, 2016

@goodmind You may want to consider Discriminated Unions to solve this kind of problem. They let you get strong typing in switch cases without writing type guards when you have a common string literal type property to discriminate between them.

Here's the example from the TypeScript Handbook that I linked to:

interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}

type Shape = Square | Rectangle | Circle;

function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

@goodmind
Copy link

goodmind commented Nov 16, 2016

@rob3c I can't specify kind property because I dealing with external API. I think #12114 is what I need (maybe not).

I found issue about it #8934. Unfortunately it is closed :(

@e-cloud
Copy link
Contributor

e-cloud commented Nov 30, 2016

I've got some code similar to @icholy's sample

function dosothing(exprValue: string | boolean | number | RegExp){
    switch (typeof exprValue) {
        case 'number':
            return setNumber(exprValue);
        case 'string':
            return setString(exprValue);
        case 'boolean':
            return setBoolean(exprValue);
    }
}

function setNumber(val: number){
    // ...
}

function setString(val: string){
    // ...
}

function setBoolean(val: boolean){
    // ...
}

You can see it online in Playground

Got this error:

TS2345:Argument of type 'string | number | boolean | RegExp' is not assignable to parameter of type 'number'.
  Type 'string' is not assignable to type 'number'.

@RyanCavanaugh what's the progress about this topic?

@electricessence
Copy link

electricessence commented Nov 30, 2016

@icholy in other languages, switch cases will be isolated to constants. Having cases that are evaluated at run-time in the way shown would be done with if statements instead.

A side note about switch: depending on the underlying compiler and how it's applied, switch can actually be a very fast operation compared to other methods. But if adding run-time evaluations you're basically building a complex if block.

@icholy
Copy link
Author

icholy commented Nov 30, 2016

But if adding run-time evaluations you're basically building a complex if block.

That's a valid use case.

switch (true) {
  case isNumber(a):
    // use number
}

if (isNumber(a)) {
  // use number
}

These are functionally equivalent and the type system should support them both.

@electricessence
Copy link

electricessence commented Nov 30, 2016

@icholy I understand why you're doing what you are. It's a bit unconventional. But I just wanted to be clear that type-guards in this way may end up being more difficult to implement than an if chain.

Here's why C# only allows constants/statics:
http://stackoverflow.com/questions/44905/c-sharp-switch-statement-limitations-why

@icholy
Copy link
Author

icholy commented Nov 30, 2016

@electricessence I don't personally use that style, but apparently some people do. My point is that the type system should not be limited to what you think people should be doing.

edit: not sure why you keep going into the details of switch's implementations. It's pretty irrelevant in this context. But yay for jump tables!

@electricessence
Copy link

electricessence commented Nov 30, 2016

@icholy I am bringing it up just as a point of contrast. What I think people should or shouldn't be doing is irrelevant. Because JS can evaluate dynamic case values, I agree that it would be nice that your example actually work. I'm only suggesting that 1) it may be more difficult to implement than you may be assuming, and 2) in the scheme of other languages is unconventional and because a working version of it can be written correctly using if blocks, it may be of a lower value to fix compared to other issues/features.

Having it work with constants IMO is expected, hence why I'm on this thread. Having it work with dynamic values at run-time would be cool, but I don't have that expectation.

Oh and if there's ever any doubt. I love switch statements. :)

@RyanCavanaugh
Copy link
Member

@iFreilicht that first part only works because we can "see" the 0 initializer. The switch does nothing.

@iFreilicht
Copy link

Ah yes, you're both right, as evidenced by this:

function calc(n: number) { }
let x: string | number = "nope";

switch (typeof x) {
    case 'number':
        calc(x); //Argument of type 'string' is not assignable to parameter of type 'number'.
}

Thanks for clearing that up!

@danielmhanover
Copy link

Any update on enabling the ternary if operator as a type guard?

@RyanCavanaugh
Copy link
Member

@danielmhanover example? That should work already

@snewell92
Copy link

@danielmhanover Using type predicates works as you would expect (if I'm guessing your intent correctly):

export type SomeUnion = TypeA | TypeB;

function isTypeA(input: SomeUnion): input is TypeA {
  return (<TypeA>input).propA !== undefined;
}

export const someFunc = (input: SomeUnion) =>
  isTypeA(input)
    ? onlyTypeA(input)
    : otherCase(input);

@JannicBeck
Copy link

Hey, looking at
https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions

Can one get this example to work with an additional generic type S?
Use case would be writing a higher order reducer which calculates something if it knows the action and else just delegates it to the provided reducer. This is pretty common in redux.

interface Shape {
  kind: string;
}

interface Square extends Shape {
  kind: "square";
  size: number;
}

interface Circle extends Shape {
  kind: "circle";
  radius: number;
}

// adding | S here causes the cases to loose their type and everything is just ShapeType<S>
type ShapeType<S extends Shape> = Square | Circle | S;

function area<S extends Shape>(s: ShapeType<S>) {
  switch (s.kind) {
    case "square":
      return s.size * s.size; // s should be of type Square
    case "circle":
      return Math.PI * s.radius ** 2; // s should be of type Circle
    default:
      return 0 // s should be of type S
  }
}

@snewell92
Copy link

Could we combine Discriminated Unions and Type Predicates to get fully exhaustive, statically checked poor-man's pattern matching in Typescript? I don't think this could generalize to any tagged union, but perhaps it could be a good starting point to get most of the plumbing out of the way?

@jack-williams
Copy link
Collaborator

cc @RyanCavanaugh @mhegazy

I'm considering having a go at the original proposal of switch on typeof. E.g:

function logNumber(v: number) { console.log("number:", v); }
function logString(v: string) { console.log("string:", v); }

function foo1(v: number|string) {
    switch (typeof v) {
        case 'number':
            logNumber(v);
            break;
        case 'string':
            logString(v);
            break;
        default:
            throw new Error("unsupported type");
    }
}

Is this still wanted by the TS team?

@mhegazy
Copy link
Contributor

mhegazy commented Feb 13, 2018

Is this still wanted by the TS team?

Yes.

@jack-williams
Copy link
Collaborator

jack-williams commented Feb 13, 2018

Great, I'll give it a shot.

EDIT: PR submitted

@mattacosta
Copy link

Just for future reference, there's another similar scenario:

switch (obj.constructor) {
  case DerivedType:
    // obj should be a DerivedType
    break;
  default:
    // obj is still a base type
    break;
}
// obj is still a base type

@jack-williams
Copy link
Collaborator

jack-williams commented May 21, 2018

@mhegazy If this is still on the TS radar, would it be possible to get a reviewer assigned to this issue?

@lukescott
Copy link

I'm not sure if @JannicBeck 's issue of discriminated unions is related to this one, but is that being tracked anywhere? I run into the issue he mentions all the time, specifically with Redux. I have a reducer that handles certain actions, but then defers to another reducer of unknown types.

@ghost ghost mentioned this issue Aug 7, 2018
4 tasks
@ghost
Copy link

ghost commented Aug 7, 2018

@lukescott One workaround would be to just cast the general type to a known union type: const s = sIn as any as Square | Circle;. But I've made an issue at #26277.

RyanCavanaugh added a commit that referenced this issue Sep 6, 2018
Fix #2214. Support narrowing with typeof in switch condition.
@miguel-leon
Copy link

Hi, Is there any way the following can be allowed without error?

class A {
	aa = 5;
}

class B {
	bb = 9;
}


function doStuff(o: A | B): number {
	switch (o.constructor) {
		case A:
			return o.aa; // error!
		default:
			return o.bb; // error!
	}
}

console.log(doStuff(new A()));

Please add type guard narrowing with switch case of the constructor property.

@jack-williams
Copy link
Collaborator

@miguel-leon I believe your suggestion is tracked by #23274

@danny-huang-openfind
Copy link

danny-huang-openfind commented Jun 15, 2023

I met a specific case, and I'm wonder if this can be fix.

enum Test = {
  TestA = 'TESTA',
  TestB = 'TESTB',
}
type TestValues = `${Test}`;
type DataType<TypeName> = TypeName extends TestValues
  ? TypeName extends 'TESTA'
    ? TestAData
    : TypeName extends 'TESTB'
    ? TestBData
    : never
  : never;

function exampleForTestA(data: TestAData) {
  // do something...
}

function exampleForTestB(data: TestBData) {
  // do something...
}

function example<T extends TestValues, D extends DataType<T>>(
  type: T,
  data: D,
) {
  switch (type) {
    case 'TESTA':
      return exampleForTestA(data as TestAData); // I have to add type assertion to make it work without error
    case 'TESTB':
      return exampleForTestB(data as TestBData); // same here
    default:
      throw new Error('Unknown type');
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests