TypeScript interface that allows other properties

Typescript

Typescript Problem Overview


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?

Typescript Solutions


Solution 1 - Typescript

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 - Typescript

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

Solution 3 - Typescript

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}

Solution 4 - Typescript

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>;

Solution 5 - Typescript

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.

Solution 6 - Typescript

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;

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAaron BeallView Question on Stackoverflow
Solution 1 - TypescriptSebastienView Answer on Stackoverflow
Solution 2 - TypescriptzloctbView Answer on Stackoverflow
Solution 3 - TypescriptCristian TraìnaView Answer on Stackoverflow
Solution 4 - TypescriptbrendangibsonView Answer on Stackoverflow
Solution 5 - TypescriptAaron BeallView Answer on Stackoverflow
Solution 6 - TypescriptmPrinCView Answer on Stackoverflow