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?


Solution 1:

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

Solution 2:

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

Solution 3:

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}