Reputation: 3787
I'm running into a problem where trying to use a Typescript type with an index signature to constrain the values of an object literal destroys the ability, later on, to correctly infer the exact keys of that object. I'm wondering if there's something clever I could do to work around this (I'm not wedded to index signatures), or if I'm hitting a limitation of the language.
For context, I'm writing a function (createStore
) that accepts an actions
parameter that is an object containing some number of StoreAction
functions as values. The createStore
function returns a store
object that contains (among other things) those same actions. When I use the store
, I want TypeScript to yell at me if I call an an action that doesn't exist (i.e. it isn't a key on the actions
object that was used to create the store
).
You can see the example below in the TypeScript Playground.
Here is a simplified version of the createStore
function:
type StoreAction<State> = (state: State) => State;
type ValuesAreActions<T, State> = {
[key in keyof T]: StoreAction<State>;
};
function createStore<State, Actions extends ValuesAreActions<Actions, State>>(actions: Actions, state: State) {
// In the real implementation, we're building the store, and binding the action functions to the store.
return { actions, state };
}
Using it this way works fine:
type MyStoreState = { foo: string }
// Note: no type annotations on the "actions" object literal
const actions = {
myAction1: (state: MyStoreState) => { /* do some modifications to state... */ return state },
}
const myStore = createStore(actions, { foo: "bar"});
myStore.actions.myAction1({ foo: "baz"}) // Works, as expected
myStore.actions.invalidAction({ foo: "baz"}) // TS Error, as expected
This does not work as I'd like:
// I attempted to write a type that constrains the value of "actions" object literal,
// so the user can get feedback inline if they make a mistake in the action function signatures.
type StoreActions<State> = {
[name: string]: StoreAction<State>
}
// Note: unlike above, this object literal is given an index signature.
const actions2: StoreActions<MyStoreState> = {
myAction1: state => { /* do some modifications to state... */ return state }
}
const myStore2 = createStore(actions2, { foo: "bar"})
myStore2.actions.myAction1({ foo: "baz"}) // Works, as expected
myStore2.actions.invalidAction({ foo: "baz"}) // No TS Error!
Upvotes: 0
Views: 99
Reputation: 3787
It looks like TypeScript 4.9+ will have exactly the feature I was asking for - the satisfies
operator (see blog post).
For example:
// This type is meant to constrain the value of the "actions" object (same as above)
type StoreActions<State> = {
[name: string]: StoreAction<State>
}
// This is the "old" way of doing it (same as above). Assigning a general type to "actions2" hides the more specific aspects of the type.
const actions2: StoreActions<MyStoreState> = {
myAction1: state => { /* do some modifications to state... */ return state }
}
// This uses the new "satisfies" operator to help catch errors on the object literal, while still enabling type inference down the line.
const actions3 = {
myAction1: state => { /* do some modifications to state... */ return state }
} satisfies StoreActions<MyStoreState>;
const myStore2 = createStore(actions2, { foo: "bar"});
const myStore3 = createStore(actions3, { foo: "bar"});
myStore2.actions.myAction1({ foo: "baz"}) // Works, as expected.
myStore3.actions.myAction1({ foo: "baz"}) // Works, as expected.
// @ts-expect-error
myStore2.actions.invalidAction({ foo: "baz"}) // No TS Error!
// @ts-expect-error
myStore3.actions.invalidAction({ foo: "baz"}) // Property 'invalidAction' does not exist on type '{ myAction1: (state: MyStoreState) => MyStoreState; }'
See this example in the typescript playground
Upvotes: 1
Reputation: 45262
Not sure I totally understand what you're trying to do, but here's a 'throw spaghetti at a wall' suggestion..
Make the key type of StoreActions
a generic parameter:
type StoreActions<KeyType extends string, State> = {
[key in KeyType]: StoreAction<State>
}
Now pass the allowable keys as a generic parameter:
const actions2: StoreActions<
"myAction1" | "myAction2",
MyStoreState
> = {
myAction1: state => { /* do some modifications to state... */ return state },
myAction2: state => { /* I'm adding a second valid key here... */ return state }
}
Upvotes: 1