In the code below, a StoreOp<"vanilla">
should be able to operate on a Store
with "vanilla"
as a flag, but which has other flags too. Currently the constraints are wrong, and a StoreOp<"vanilla">
can only work on a FlagStore<"vanilla">
when it should be able to also work on a FlagStore<"vanilla", "chocolate">
.
Calls to setVanilla
and setChocolate
below currently have unwanted errors from this mismatch, and a successful solution would eliminate errors as commented against the four final lines of code.
An example error is like this...
Argument of type 'Store<{ vanilla: boolean; chocolate: boolean; raspberry: boolean; }>' is not assignable to parameter of type 'FlagStore<"vanilla">'.
Types of property 'write' are incompatible.
How can I express constraints on the StoreOp
generic function type, and the corresponding createSetterOp
factory function so that any Store having the Operated
flags AMONG its Stored
flags is a valid late-bound argument to the StoreOp
. Currently the constraint S extends FlagStore<Operated>
is the wrong way round, but I'm hitting a dead end how to constrain it the other way around - that S
includes Operated
and MORE.
A StoreOp
may rely on every one of its Operated
flags having a corresponding boolean in the store, but shouldn't care what other flags are ALSO available. In terms of the StoreOp
generic function definition, Operated
should be assignable to Stored
not the other way around. I am expecting the late-bound typing of the generic function against the actual store to ensure that a StoreOp
implementation satisfies the Store's type when making an edit - e.g. it will be a compiler error unless it copies also the other (unknown) flags when writing back to the store.
In my wider API I need inference from both the Stored
flags and the Operated
flags to drive other typing, hence the crux being in this type widening of the StoreOp
.
The code below is also in this Typescript Playground
interface Store<T> {
write: (state: T) => void;
read: () => T;
}
type Flag = "vanilla" | "chocolate" | "raspberry";
type FlagStore<Stored extends Flag> = Store<{
[flag in Stored]: boolean;
}>;
type StoreOp<Operated extends Flag> = <S extends FlagStore<Operated>>(
store: S
) => void;
/** Create some stores */
function createStore<State>(state: State): Store<State> {
const ref = { state };
return {
read: () => ref.state,
write: (state: State) => ref.state = state,
};
}
const fewStore = createStore({
vanilla: false,
});
const manyStore = createStore({
vanilla: false,
chocolate: false,
raspberry: false,
});
/** Prove store operations - seem correct */
// No errors as expected
fewStore.write({
vanilla: true,
});
// No errors as expected
manyStore.write({
vanilla: true,
chocolate: false,
raspberry: true
});
fewStore.write({
vanilla: true,
chocolate: false, // error here is correct - excess property
});
// error here is correct - missing property
manyStore.write({
vanilla: true,
chocolate: false,
});
/** STOREOP DEFINITION, USE AND ERRORS */
function createSetterOp<Operated extends Flag>(
flag: Operated
): StoreOp<Operated> {
return <S extends FlagStore<Operated>>(store: S) => {
store.write({
...store.read(),
[flag]: true,
});
};
}
const setVanilla = createSetterOp("vanilla");
const setChocolate = createSetterOp("chocolate");
setVanilla(manyStore); // this should NOT error - manyStore has extra keys but that's fine
setVanilla(fewStore); // this should NOT error - fewStore has the 'vanilla' key
setChocolate(manyStore); // this should NOT error - manyStore has extra keys but that's fine
setChocolate(fewStore); // this SHOULD error as the fewStore doesn't have the chocolate key
` that has AT LEAST `Operated` among its keys. I don't know how to constrain a type to say it should allow assignment of another type, not be assignable to it (extended from it), in this context.