71

I have an interface Action:

interface Action {}

And an implementation of this Action SpecificAction:

class SpecificAction implements Action {
   payload?: Any
}

Is it possible in TS to construct a switch operator, like this:

let action: Action
switch (action) {
   case SpecificAction: //it works
       console.log(action.payload) // it doesn't 
}

Is it possible in that case to know, that action is already of SpecificAction type?

10
  • 1
    Could you use a discriminated union? Commented Jun 9, 2018 at 13:42
  • 1
    I could, but I don't really see how it is better. I will still have to check some property instead of type itself Commented Jun 9, 2018 at 13:44
  • 2
    @VladyslavZavalykhatko yes you will still have to check some property, but that is what the compiler supports for switch and type guards Commented Jun 9, 2018 at 14:42
  • 1
    @TitianCernicova-Dragomir, fair enough) Commented Jun 9, 2018 at 16:57
  • Is action in practice an instance of a class that implements Action, or could it just be a regular object? If the former, did my answer help, or did I misunderstand what you were looking for? Commented Jun 19, 2018 at 16:46

3 Answers 3

55

for the time being it looks like there are a few options, all of them with some drawbacks

  • discriminated unions (docs, stackblitz), but you'll need a dedicated property as discriminator
interface Action {}

class SpecificAction implements Action {
  kind: "specific";
  payload?: any;
}

class ToggleAction implements Action {
  kind: "toggle";
  toggle: boolean;
}

let action: SpecificAction | ToggleAction;
switch (action.kind) {
  case "specific":
    console.log(action.payload) // it works 
    break;
  case "toggle":
    console.log(action.toggle) // it works 
    break;        
}
  • User-Defined Type Guards (docs, stackblitz), but you'll need if statements instead of switch
interface Action {}

class SpecificAction implements Action {
  payload?: any;
}

class ToggleAction implements Action {
  toggle: boolean;
}

let isSpecific = (p: any): p is SpecificAction => !!p.payload
let isToggle = (p: any): p is ToggleAction => !!p.toggle

let action: Action;
if (isSpecific(action)) {
  console.log(action.payload) // it works 
} else if (isToggle(action)) {
  console.log(action.toggle) // it works 
}
  • constructor property (github, stackblitz), but you'll need to cast to desired type for the time being
interface Action { }

class SpecificAction implements Action {
  payload?: any;
}

class ToggleAction implements Action {
  toggle: boolean;
}

switch (action.constructor) {
  case SpecificAction:
    console.log((<SpecificAction>action).payload) // it kinda works 
    break;
  case ToggleAction:
    console.log((<ToggleAction>action).toggle) // it kinda works 
    break;
  }
Sign up to request clarification or add additional context in comments.

3 Comments

An empty interface is nonsensical in typescript as it in no way unifies implementers as evidenced by the optional nature of the implements clause.
i'm pretty sure we miss some context in the original question, but for completeness i guess this github.com/Microsoft/TypeScript/wiki/FAQ#what-is-type-erasure should help any future reader for that matter
Switching with constructor is my preferred way but be aware that this may not work in special cases, for example consider this one: export class Response { static Success = class extends Response { } } console.log(new Response.Success().constructor); console.log(new Response().constructor);
10

You'd be better off using an if statement with typeguards.

let action: Action = ...;
if (isSpecificAction(action)) {
    console.log(action.payload);
}

function isSpecificAction(action: any): action is SpecificAction {
    return action.payload;
}

At the end of the day, TypeScript is still JavaScripty, and the switch statement gets transpiled to a regular JS switch:

A switch statement first evaluates its expression. It then looks for the first case clause whose expression evaluates to the same value as the result of the input expression (using the strict comparison, ===)

So in your case:

interface Action {}
class SpecificAction implements Action {
   payload?: any
}

let action: Action
switch (action) {
   case SpecificAction: //it works
       console.log(action.payload) // it doesn't 
}

action would be evaluated and compared with the class SpecificAction. Presumably, action is an instance of SpecificAction (or some object that implements the Action interface).

With a switch, you could do this:

let a: Action = new SpecificAction();
let b: Action = a;

switch (a) {
    case b:
        console.log("Worked");
}

The expression a is evaluated and compared to the expression b (and a === b, so we hit the console.log), but that's obviously not what you're looking for.

If you want to check if an instance is of a particular type (re: class), then you should use a type guard. A switch/case is the wrong construct.


Alternatively, why not use instanceof?

interface Action { };
class SpecificAction implements Action {}
class NotSpecificAction implements Action {}

let action: Action = new SpecificAction();
console.log(action instanceof SpecificAction); // true
console.log(action instanceof NotSpecificAction); // false

4 Comments

Does this not answer the question, or are there technical inaccuracies? (curious about the downvote)
This is an excellent answer but it's worth pointing out the misunderstanding evident in the use of the marker interface in the OP as opposed to repeating it. An empty interface is nonsensical in typescript as it in no way unifies implementers as evidenced by the optional nature of the implements clause
@AluanHaddad The marker interface may just be a minimal example. It's quite possible that his actual Action interface has more to offer
that's possible but given the nature and phrasing of the question, I think it is important to point out. Many people don't understand type erasure, and are confused by this. I would write my own answer, but I like yours. +1 regardless
0

If you need only typescript level checking you can play with:

interface Payload {
  id: string;
  title: string;
  description: string;
}

enum EAction {
  CREATE = 'CREATE',
  UPDATE = 'UPDATE',
  DELETE = 'DELETE',
  CLEAR_ALL = 'CLEAR_ALL',
}

interface IActionsPayloads {
  [EAction.CREATE]: Payload;
  [EAction.UPDATE]: Pick<Payload, 'title' | 'description'>;
  [EAction.DELETE]: Pick<Payload, 'id'>;
  [EAction.CLEAR_ALL]: never;
}

type TAction<T extends EAction> = IActionsPayloads[T] extends never ? { type: T; } : { type: T; payload: IActionsPayloads[T] }

And example usage:

const action1: TAction<EAction.CREATE> = {
    type: EAction.CREATE,
    payload: {
        id: '1',
        title: 'shopping',
        description: 'go to the closest market'
    }
}

const action2: TAction<EAction.DELETE> = {
    type: EAction.DELETE,
    payload: {
        id: '1'
    }
}

const action3: TAction<EAction.CLEAR_ALL> = {
    type: EAction.CLEAR_ALL,
}

Also with function helper:

function createAction<T extends EAction>(action: TAction<T>): TAction<T> {
    return action;
}

const action4 = createAction({ type: EAction.CLEAR_ALL });

Comments

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.