how can I implement types on classes?

Viewed 411

I have this basic class:


type Action = { type: string; payload?: {} }
type Dispatch<ActionType extends Action> = (action: ActionType) => void


type Actions = 
  |  { type: '@ADD', payload: { item: string, id: string } }
  |  { type: '@REM', payload: { id: string } }

class CardActions implements ActionClass<Actions> { // this isn't working as I expect, see below
  constructor(private dispatch: Dispatch<Actions>) {}

  disp(action) {} // this isn't working
}

trying to implement an interface didn't work neither for the constructor nor the disp function, it always has the error:

Index signature is missing in type 'CardsActions'

or

Class 'CardActions' incorrectly implements interface 'ActionClass'.

export interface ActionClass<ActionType extends Action> {
  // new(dispatch: string): any;
  [actionKey: string]: (action: ActionType['type']) => void
}

I'm expecting it to work like any function, did I misunderstand it?

Expectation

interface Func {
  (action: Actions['type']): void
}

const func: Func = (action) => {} // this will work

playground

why they don't work like eachother? and how can I make it work?

1 Answers

For this answer I will use a slightly modified version of ActionClass that removes the generics, as they are not relevant to the particular issue you're running into:

interface ActionClass {
  [actionKey: string]: (action: Actions['type']) => void
}

One current limitation of TypeScript is that when a class implements an interface, the compiler does not use the definition of the interface to contextually type the members of the class. In class Foo implements Bar {...}, the compiler will infer a type for Foo without consulting Bar at all; and only afterward will it check that type against Bar. See microsoft/TypeScript#32082 and the many linked issues within to read more about this.

For now, if you want class Foo implements Bar {...} to work, you will need to manually and explicitly give Foo's members everything the compiler needs so that the inferred type of Foo is assignable to Bar.

Right now CardActions is inferred as if you wrote this:

class CardActions {
  constructor(private dispatch: Dispatch<Actions>) { }
  disp(action) { }
}

Because you did not give CardActions an index signature, the compiler does not infer one. So then CardActions is not seen as assignable to ActionClass, which does have one:

class CardActions implements ActionClass { // error
  constructor(private dispatch: Dispatch<Actions>) { } 
  disp(action) { }
}

Additionally, the action parameter of disp() is inferred as any because the compiler does not consult ActionClass at all, and you didn't annotate action.

So the first steps to a fix here would be to give CardActions an explicit index signature, and give the action parameter of disp() an explicit type:

class CardActions implements ActionClass {
  constructor(private dispatch: Dispatch<Actions>) { } // error
  [actionKey: string]: (action: Actions['type']) => void;
  disp(action: Actions['type']) { }
}

Now we have a new error.


Note that ActionClass has a string index signature. That means all properties of an ActionClass object must be a function of the type (action: Actions['type']) => void. But in your constructor, you have put the private keyword on the dispatch parameter, making it a parameter property. That's shorthand for something like:

class CardActions implements ActionClass {
  private dispatch: Dispatch<Actions>; // error
  constructor(dispatch: Dispatch<Actions>) {
    this.dispatch = dispatch;
  }
  [actionKey: string]: (action: Actions['type']) => void;
  disp(action: Actions['type']) { }
}

And here's your problem. The dispatch property is of type Dispatch<Actions>, but the index signature requires that every property be assignable to (action: Actions['type']) => void. And a Dispatch<Actions> is not assignable to that (the parameter type is wrong).

I'm not sure what the right way to fix this is, since I'm not sure whether you really want the index signature as written or you really want a dispatch property. But you can't have both without an error. Let's remove the dispatch property just to see that the error goes away:

class CardActions implements ActionClass {
  constructor(dispatch: Dispatch<Actions>) { }
  [actionKey: string]: (action: Actions['type']) => void;
  disp(action: Actions['type']) { }
}

Hooray, no error! There are probably a hundred different possible approaches that one might want to take instead of this, but exploring those is probably out of scope for this question.


So there you go. If you want a class to implement an interface that has a string index signature, you need to explicitly declare the index signature in the class definition, and you need to make sure that every single known property of the class is compatible with the index signature.

Playground link to code

Related