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 · 53 comments

Comments

Projects
None yet
@icholy
Copy link

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

This comment has been minimized.

Copy link

commented Mar 21, 2015

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

@zspitz

This comment has been minimized.

Copy link
Contributor

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 this to the Community milestone May 4, 2015

@RyanCavanaugh

This comment has been minimized.

Copy link
Member

commented May 4, 2015

Approved

@AbraaoAlves

This comment has been minimized.

Copy link

commented Dec 17, 2015

+1

@jedmao

This comment has been minimized.

Copy link
Contributor

commented Jan 23, 2016

Is this the same as #2388?

@DanielRosenwasser

This comment has been minimized.

Copy link
Member

commented Jan 23, 2016

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

@goodmind

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

commented Nov 15, 2016

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

@goodmind

This comment has been minimized.

Copy link

commented Nov 15, 2016

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

@icholy

This comment has been minimized.

Copy link
Author

commented Nov 15, 2016

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

@rob3c

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Author

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Author

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

This comment has been minimized.

Copy link

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. :)

@mhegazy

This comment has been minimized.

Copy link

commented Feb 1, 2017

The feature tracked by this issue does not handle this either. narrowing in general, whether it is in switch statements or not, only works on union types. so component has to be defined as a union type for component.name check to do anything.

This feature, as noted in the OP, is to enable typeof to work in a switch statement.

@icholy

This comment has been minimized.

Copy link
Author

commented Feb 1, 2017

@pleerock what's wrong with instanceof ?

private onChildActivation(component: any) {
  if (component instanceof BodyStateAddEditModalComponent) {
    component.bodyState = this.bodyState;
  }
  else if (component instanceof BodyStateInformationPageComponent) {
    // do nothing
  }
  else if (component instanceof PostListSubPageComponent) {
    this.loadPosts(component);
  }
}
@pleerock

This comment has been minimized.

Copy link

commented Feb 2, 2017

@icholy yeah that works, however with switch case it could be much cleaner

@iFreilicht

This comment has been minimized.

Copy link

commented May 17, 2017

This already somewhat works as of TS 2.3.2, but only outside functions:

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

switch (typeof x) {
    case 'number':
        calc(x); //works as expected
}

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

I'm not sure why it works when the switch statement is on the top level or if that is even intentional, but that might be useful info for anyone trying to implement this feature.

@chriswep

This comment has been minimized.

Copy link

commented May 17, 2017

@iFreilicht i suppose that it has to do with the variable x being outside the scope of the function, not the switch being inside. If i'm not wrong here, you can never be 100% sure about the value of x since it can be changed from anywhere anytime. I would expect it to work if you put the let x inside the function or as a parameter (didn't test).

@RyanCavanaugh

This comment has been minimized.

Copy link
Member

commented May 17, 2017

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

@iFreilicht

This comment has been minimized.

Copy link

commented May 17, 2017

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

This comment has been minimized.

Copy link

commented Aug 29, 2017

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

@RyanCavanaugh

This comment has been minimized.

Copy link
Member

commented Aug 29, 2017

@danielmhanover example? That should work already

@snewell92

This comment has been minimized.

Copy link

commented Dec 14, 2017

@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

This comment has been minimized.

Copy link

commented Dec 15, 2017

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

This comment has been minimized.

Copy link

commented Dec 15, 2017

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

This comment has been minimized.

Copy link
Contributor

commented Feb 13, 2018

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

This comment has been minimized.

Copy link

commented Feb 13, 2018

Is this still wanted by the TS team?

Yes.

@jack-williams

This comment has been minimized.

Copy link
Contributor

commented Feb 13, 2018

Great, I'll give it a shot.

EDIT: PR submitted

@mattacosta

This comment has been minimized.

Copy link

commented Apr 10, 2018

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link

commented Aug 7, 2018

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.

@andy-ms andy-ms referenced this issue Aug 7, 2018

Open

Support open-ended unions #26277

4 of 4 tasks complete
@andy-ms

This comment has been minimized.

Copy link
Member

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

Merge pull request #21957 from jack-williams/typeof-in-switch
Fix #2214. Support narrowing with typeof in switch condition.
@miguel-leon

This comment has been minimized.

Copy link

commented Oct 19, 2018

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

This comment has been minimized.

Copy link
Contributor

commented Oct 21, 2018

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.