138

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?

Aaron Beall
  • 49,769
  • 26
  • 85
  • 103

6 Answers6

259

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

Ingo Bürk
  • 19,263
  • 6
  • 66
  • 100
Sebastien
  • 5,506
  • 4
  • 27
  • 37
  • 1
    Thanks, this is good info. It answer my over-simplified title exactly... it turns out there was a way to use the extended `` in the details of my question by simply casting the object literal. Sorry for the clumsy question. Should I try to fix the title and summary, ex "How to both implement a sub-interface and satisfy a super-interface with an object literal", or just accept your answer? – Aaron Beall Nov 24 '15 at 21:34
  • One or the other is fine with me. Changing the title is probably better to reflect the questions. – Sebastien Nov 25 '15 at 08:57
  • 9
    Sadly this isn't working if we add fields of other types, like having one Date property along with dynamic string properties – K. D. Dec 31 '19 at 11:19
  • 2
    type: `unknown` would be better than `any`. – Burnee Oct 10 '21 at 15:02
  • @K.D. it does actually work so long as the indexer type is any or unknown. Making it `string` for example would do as you said. – zr0gravity7 Aug 22 '22 at 05:20
10
interface Options {
  darkMode?: boolean;
  [otherOptions: string]: unknown;
  }
zloctb
  • 10,592
  • 8
  • 70
  • 89
4

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>;
brendangibson
  • 2,377
  • 2
  • 21
  • 36
  • 2
    This doesn't seem to work: [https://www.typescriptlang.org/play?#code/C4TwDgpgBAsghga2gXigI...](https://www.typescriptlang.org/play?#code/C4TwDgpgBAsghga2gXigIgGoHsA2A3LNKAH3QAVcIBnYOAJyNLQGU44AjNAbgCgBLAHbAIdAGZwAxtABCcKhADC9KAG8eUDVAC2iCAC5Yu3pqgALLHXkUA7iIMCArlvYjjmuDhwB1UxAg4AETo+PH0odixKOAFeAF8eUEgoJTooVFl5FKgAMigAJQgJCwATAB4aYIEAcwAaKArBKoA+Xh4igRptEBSDLNQ1DR0kA0xcAjQa9TMLKyxbOgMAVgAGZcmND29ffyCQsOA6BwhJ2KA) – zr0gravity7 Aug 22 '22 at 05:18
4

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;
mPrinC
  • 9,147
  • 2
  • 32
  • 31
2

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}
0

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.

Aaron Beall
  • 49,769
  • 26
  • 85
  • 103