Reputation: 52133
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
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
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 allowedAn 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
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;
}
Upvotes: 284
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
Reputation: 11177
interface Options {
darkMode?: boolean;
[otherOptions: string]: unknown;
}
Upvotes: 13
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: {}
})
Upvotes: 0