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.
Yes please allow switch case there for state-machine. Also the conditional/ternary operator (?:
).
+1 for switch
and ?:
@RyanCavanaugh @jasonwilliams200OK Isn't this the same as #2388 ?
@icholy In the meantime, you could use explicit type casting:
typescript
switch (typeof v) {
case 'number':
logNumber(<number>v);
break;
case 'string':
logString(<string>v);
break;
default:
throw new Error("unsupported type");
}```
Approved
+1
Is this the same as https://github.com/Microsoft/TypeScript/issues/2388?
Not quite the same thing, since you don't need #2388 for this.
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')
}
@goodmind Your above suggestion breaks common switch
rules. Cases have to be constants.
@electricessence ok. i think https://github.com/Microsoft/TypeScript/issues/165 better for something like my example (but without switch, maybe auto-generated user type guards or so)
@electricessence what do you mean by "common switch
rules"? The example provided by @goodmind is valid code.
@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;
}
}
@rob3c I can't specify kind
property because I dealing with external API. I think https://github.com/Microsoft/TypeScript/pull/12114 is what I need (maybe not).
I found issue about it #8934. Unfortunately it is closed :(
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?
@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.
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.
@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
@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!
@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. :)
@electricessence it's not a dynamic value at runtime. It's a boolean literal which is it's own type so it's known at compile time. Obviously something like this wouldn't work.
function getTrueOrFalse(): boolean {
return Math.random() > 0.5;
}
switch (getTrueOrFalse()) {
case isNumber(a):
// use number
}
@icholy So then I guess the important question for the above example is, wouldn't this be a more normal case? Passing a constant or literal (true
) value to a switch is redundant. This above example is not.
I found out that discriminate unions and switch type guards as implemented in the ngrx-example app (see https://github.com/ngrx/example-app/blob/master/src/app/reducers/books.ts#L20) worked in TS 2.0.0 but stopped working in TS >= 2.1.
Is this expected behaviour? Is this because the discriminator is a just a string property on a class instead of being a type in the interface?
@metzc I have the same problem, also using code based on the ngrx example app! For the time being I'm sticking with TS2.0.10 until this is sorted out.
@metzc Unfortunately, they've made breaking changes in TypeScript 2.1+ with how string literal types are narrowed for reasons that I don't agree with. The pattern in the ngrx example app can still be made to work, albeit at the expense of a bunch of unnecessarily noisy cruft. See this playground for the fix, if you can call it that. Here's the full playground code with the newly broken version as well as the hoops you have to jump through to get string literal types behaving again. I'd love to know a better way, but unless the TS team reverts some decisions, I think this popular use case is out of luck.
const actionTypeCache: { [type: string]: boolean } = {};
function ensureSingletonType<T extends string>(actionType: T): T {
if (actionTypeCache[<string>actionType]) {
throw new Error(`Action type "${actionType}" is not unique"`);
}
actionTypeCache[<string>actionType] = true;
return actionType;
}
/** placeholders */
interface Action { type: string; payload?: any; }
interface Book { id: string; }
interface State { }
function reduceSearch(state: State, id: string): State { return state; }
function reduceLoad(state: State, book: Book): State { return state; }
/******************** BEGIN BROKEN ********************/
// In TS 2.1+ these properties are only strings instead of literals,
// even though `ensureSingletonType` returns them as literals.
const ActionTypesBROKEN = {
SEARCH: ensureSingletonType('[Book] Search'),
LOAD: ensureSingletonType('[Book] Load')
};
class SearchActionBROKEN implements Action {
// In TS 2.1+ this is only a string instead of a string literal
type = ActionTypesBROKEN.SEARCH;
constructor(public payload: string) { }
}
class LoadActionBROKEN implements Action {
// In TS 2.1+ this is only a string instead of a string literal
type = ActionTypesBROKEN.LOAD;
constructor(public payload: Book) { }
}
type ActionsBROKEN = SearchActionBROKEN | LoadActionBROKEN;
function reducerBROKEN(state = {}, action: ActionsBROKEN): State {
switch (action.type) {
case ActionTypesWORKS.SEARCH: {
const id = action.payload; // still string | Book
return reduceSearch(state, id); // type narrowing is BROKEN!
}
case ActionTypesWORKS.LOAD: {
const book = action.payload; // still string | Book
return reduceLoad(state, book); // type narrowing is BROKEN!
}
default:
return state;
}
}
/******************** END BROKEN ********************/
/******************** BEGIN WORKS ********************/
/**
* Let's declare our string literals in one place to avoid errors.
* Regardless, duplicating string literals everywhere works the same way.
*/
const SEARCH_ACTION = ensureSingletonType('[Book] Search');
const LOAD_ACTION = ensureSingletonType('[Book] Load');
// In TS 2.1+ these crufty declarations are required to preserve string literals!
const ActionTypesWORKS = {
SEARCH: <typeof SEARCH_ACTION>SEARCH_ACTION, // TS 2.1+ required cruft
LOAD: <typeof LOAD_ACTION>LOAD_ACTION, // TS 2.1+ required cruft
};
class SearchActionWORKS implements Action {
// TS 2.1+ required cruft
type: typeof ActionTypesWORKS.SEARCH = ActionTypesWORKS.SEARCH;
constructor(public payload: string) { }
}
class LoadActionWORKS implements Action {
// TS 2.1+ required cruft
type: typeof ActionTypesWORKS.LOAD = ActionTypesWORKS.LOAD;
constructor(public payload: Book) { }
}
type ActionsWORKS = SearchActionWORKS | LoadActionWORKS;
function reducerWORKS(state = {}, action: ActionsWORKS): State {
switch (action.type) {
case ActionTypesWORKS.SEARCH: {
const id = action.payload; // string
return reduceSearch(state, id); // type narrowing WORKS!
}
case ActionTypesWORKS.LOAD: {
const book = action.payload; // Book
return reduceLoad(state, book); // type narrowing WORKS!
}
default:
return state;
}
}
/******************** END WORKS ********************/
@rob3d i already found the ngrx example style to contain too much cruft but this almost kills it for me.. maybe we need to think in a different direction here to implement the same basic idea with less bloat (while still retaining full type support). Do you know of any existing alternative approaches / examples?
Without knowing enough about what the goal of the changes in 2.1.4 are I can't comment about how desirable or not they might be in the long term, but I can comment on the pace of breaking changes with TypeScript: _No sir, I don't like it_. If you're going to make changes that simply break existing code, it should be done in a major version number. Minor version numbers are supposed to be backwards-compatible.
@metzc I found a way to reduce the additional code required to preserve the literals. It leverages the intent of TS 2.1+ type widening to only widen when assigning to a mutable variable/property, while preserving literals when assigning to const
variables and readonly
properties. I thought I tried this version yesterday unsuccessfully, but I must've missed a detail somewhere because it's working today :-)
Anyhow, here is the updated playground comparing the original ngrx style with a version respecting the TS 2.1+ breaking changes.
And here's the updated _WORKS_ code from the new playground:
/******************** BEGIN WORKS ********************/
// In TS 2.1+ const variables and readonly properties are
// required to preserve literals. Unfortunately, we can't
// declare anonymous objects with readonly properties, so
// we use a class here with static readonly properties to
// to capture the actions as string literals in one place.
class ActionTypesWORKS {
static readonly SEARCH = ensureSingletonType('[Book] Search');
static readonly LOAD = ensureSingletonType('[Book] Load');
};
class SearchActionWORKS implements Action {
// TS 2.1+ requires readonly to preserve literals
readonly type = ActionTypesWORKS.SEARCH;
constructor(public payload: string) { }
}
class LoadActionWORKS implements Action {
// TS 2.1+ requires readonly to preserve literals
readonly type = ActionTypesWORKS.LOAD;
constructor(public payload: Book) { }
}
type ActionsWORKS = SearchActionWORKS | LoadActionWORKS;
function reducerWORKS(state = {}, action: ActionsWORKS): State {
switch (action.type) {
case ActionTypesWORKS.SEARCH: {
const id = action.payload; // string
return reduceSearch(state, id); // type narrowing WORKS!
}
case ActionTypesWORKS.LOAD: {
const book = action.payload; // Book
return reduceLoad(state, book); // type narrowing WORKS!
}
default:
return state;
}
}
/******************** END WORKS ********************/
good work @rob3c, that looks much better.
it is also backwards compatible to 2.0, so we can use this in projects that didn't migrate to 2.1 yet.
Looks great! Thanks very much! :)
@mischkl @rob3c on a second thought i think the change in TS may actually be a bugfix rather than a breaking change (thus the versioning would be correct i guess?). The TS compiler can't be sure that a string property of on object isn't changed in runtime. So narrowing it down based on that would be wrong. I remember that because of that i already tried to set the property to readonly on the Action class. What i overlooked though was that we were assigning a variable (ActionTypes.*) that was not readonly, so @rob3c now added this missing bit.
Something like this could be extremely useful:
private onChildActivation(component: any) {
switch (component.constructor.name) {
case "BodyStateAddEditModalComponent" then component is BodyStateAddEditModalComponent:
component.bodyState = this.bodyState;
break;
case "BodyStateInformationPageComponent" then component is BodyStateInformationPageComponent:
break;
case "PostListSubPageComponent" then component is PostListSubPageComponent:
this.loadPosts(component);
break;
}
}
This is rough example, but something in this way...
@pleerock this is already supported using tagged union types.
type BodyStateAddEditModalComponent = {
name: "BodyStateAddEditModalComponent";
bodyState: any;
}
type BodyStateInformationPageComponent = {
name: "BodyStateInformationPageComponent";
isPage: boolean;
}
type PostListSubPageComponent = {
name: "PostListSubPageComponent";
}
function onChildActivation(component: BodyStateAddEditModalComponent | BodyStateInformationPageComponent | PostListSubPageComponent) {
switch (component.name) {
case "BodyStateAddEditModalComponent":
component.bodyState = this.bodyState; // OK
break;
case "BodyStateInformationPageComponent":
break;
case "PostListSubPageComponent":
this.loadPosts(component);
break;
}
}
@mhegazy thank you, I'm aware of this feature, however I wanted to make comparison based on class names, but this way it produces too much more redundant code.
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.
@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);
}
}
@icholy yeah that works, however with switch case it could be much cleaner
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.
@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).
@iFreilicht that first part only works because we can "see" the 0
initializer. The switch
does nothing.
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!
Any update on enabling the ternary if operator as a type guard?
@danielmhanover example? That should work already
@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);
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
}
}
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?
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?
Is this still wanted by the TS team?
Yes.
Great, I'll give it a shot.
EDIT: PR submitted
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
@mhegazy If this is still on the TS radar, would it be possible to get a reviewer assigned to this issue?
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.
@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.
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.
@miguel-leon I believe your suggestion is tracked by #23274
Most helpful comment
@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: