Aaron Beall
Aaron Beall

Reputation: 52133

TypeScript interface that allows other properties

In summary, is it possible to have an interface that declares some base properties, but does not restrict additional properties? This is my current situation:

I'm using the Flux pattern, which defines a generic dispatcher:

class Dispatcher<TPayload> {
    dispatch(arg:TPayload):void { }
}

I then create a dispatcher with my own payload type, like this:

interface ActionPayload {
    actionType: string
}

const dispatcher = new Dispatcher<ActionPayload>();

Now I have some action code that should dispatch a payload with some additional data, but the ActionPayload interface only allows for actionType. In other words, this code:

interface SomePayload extends ActionPayload {
    someOtherData: any
}

class SomeActions {
    doSomething():void {
        dispatcher.dispatch({
            actionType: "hello",
            someOtherData: {}
        })
    }
}

Gives a compile-error because someOtherData does not match the ActionPayload interface. The issue is that many different "action" classes will re-use the same dispatcher, so while it's someOtherData here it might be anotherKindOfData over there, and so on. At the moment, all I can do to accomodate this is use new Dispatcher<any>() because different actions will be dispatched. All actions share a base ActionPayload, though, so I was hoping to be able to constrain the type like new Dispatcher<extends ActionPayload>() or something. Is something like that possible?

Upvotes: 157

Views: 103377

Answers (6)

brendangibson
brendangibson

Reputation: 2543

I solved this by creating something like

type Make = "Volvo" | "Polestar" | "Saab";
interface BaseCar {
    make: Make;
    horsePower: number;
    allWheelDrive: boolean;
}
type Car = BaseCar & Record<string, string|number|boolean>;

Upvotes: 4

mPrinC
mPrinC

Reputation: 9401

With TypeScript 3.9+ and strict set to true in tsconfig.json, I get errors with the accepted answer.

Here is the solution:


interface ActionPayload {
    actionType: string;

    // choose one or both depending on your use case
    // also, you can use `unknown` as property type, as TypeScript promoted type, 
    // but it will generate errors if you iterate over it
    [x: string]: any;
    [x: number]: any;
}

This avoids errors like:

  • An index signature parameter type must be either 'string' or 'number'.
    • symbol is not allowed
  • An index signature parameter type cannot be a union type. Consider using a mapped object type instead.
    • string | number is not allowed in [x: string | number]: unknown;

Upvotes: 8

Sebastien
Sebastien

Reputation: 6542

If you want ActionPayload to accept any other property you can add an indexer:

interface ActionPayload {
    actionType: string;

    // Keys can be strings, numbers, or symbols.
    // If you know it to be strings only, you can also restrict it to that.
    // For the value you can use any or unknown, 
    // with unknown being the more defensive approach.
    [x: string | number | symbol]: unknown;
}

See https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#strict-object-literal-assignment-checking

Upvotes: 284

Christian Vincenzo Traina
Christian Vincenzo Traina

Reputation: 10374

If your type is inferred from the object definition, you can assert the type as the union of the original type and {[key: string]: any]}:

const obj = { foo: 42 };
return obj as typeof obj & {[key: string]: any]};

In this way, the IntelliSense will suggest you foo, but won't complain when you try to assign or retrieve another key.

Anyway, if the additional keys are few and they're known a priori, you can just add them as optional in the type definition:

const obj: {foo: number; bar?: string} = {foo: 42}

Upvotes: 2

zloctb
zloctb

Reputation: 11177

interface Options {
  darkMode?: boolean;
  [otherOptions: string]: unknown;
  }

Upvotes: 13

Aaron Beall
Aaron Beall

Reputation: 52133

I think I found what I was looking for. I can cast the dispatched object to SomePayload, and TSC validates that its compatible with both the cast interface and the TPayload of the dispatcher:

    dispatcher.dispatch(<SomePayload>{
        actionType: "hello",
        someOtherData: {}
    })

Example online.

Upvotes: 0

Related Questions