This code example is a function for terse immutable editing of a record to set boolean values.
The function should accept a record of boolean values and a list of matching keys. It should return a new record which has all those keys set to true (using an 'immutable' update pattern in which the original record is not modified).
The errors I'm getting are so fundamental that I feel I need another pair of eyes. I must be missing something. How can I configure Generics to get the code below to compile and run sensibly?
function createNextFlags<Key extends string>(
flags: Record<Key, boolean>,
...keys: [Key, ...Key[]]
) {
const nextFlags = {
...flags
}
for (const key of keys) {
nextFlags[key] = true;
}
return nextFlags;
}
createNextFlags({
vanilla:false,
chocolate:true, // this line generates a compiler error because flags constraint is too narrow
}, "vanilla")
You can play with the problem at this playground
The errored line suggests there's a difficulty with Typescript inferring Key
too narrowly. By inferring only from the keys
array, rather than attempting to infer also from the flags
object, it ends up complaining that the flags
object is invalid if it has any property names which are not in keys
...
MOTIVATING EXAMPLE
Although this is a very simple example, the much more complex example I am working on has a similar nature, with an error condition similar to that of the excess property checking here - in other words keys
drives the constraining type of flags
when it should be inferred the other way round - keys
should be inferred from the properties of flags
.
WORKAROUNDS
You would think the workaround below would create a placeholder for the type of the flags
object....
// Define Flags explicitly
function createNextFlags<Flags extends Record<Key, boolean>, Key extends string>(
flags: Flags,
...keys: [Key, ...Key[]]
) {
const nextFlags = {
...flags
}
for (const key of keys) {
nextFlags[key] = true; // this assignment is apparently illegal!
}
return nextFlags;
}
createNextFlags({
vanilla:false,
chocolate:true,
}, "vanilla")
However, the approach creates an even weirder error. Hovering over the apparently errored assignment to the nextFlags property shows the surprising error lines (I kid you not)...
const nextFlags: Flags extends Record<Key, boolean>
Type 'boolean' is not assignable to type 'Flags[Key]'
I have also tried using keyof
to derive the keys directly from the type of flags
like this with identical results, even though it fully eliminates the Key
generic, and makes the type of keys
fully derived from flags
.
// use keyof to ensure that the property name aligns
function createNextFlags<Flags extends Record<any, boolean>>(
flags: Flags,
...keys: [keyof Flags, ...(keyof Flags)[]]
)
However, it has just the same kind of error
const nextFlags: Flags extends Record<any, boolean>
Type 'boolean' is not assignable to type 'Flags[keyof Flags]'
I have also tried using infer
to derive the keys directly from the type of flags
like this...
type InferKey<Flags extends Record<any, boolean>> = Flags extends Record<infer Key, boolean> ? Key: never;
function createNextFlags<Flags extends Record<any, boolean>>(
flags: Flags,
...keys: [InferKey<Flags>, ...InferKey<Flags>[]]
)
This leads to a similarly surprising error...
const nextFlags: Flags extends Record<any, boolean>
Type 'boolean' is not assignable to type 'Flags[InferKey<Flags>]'
What's the right way to approach this problem, so that the Key
type can be inferred from the flags
object, to constrain the keys
argument? What am I missing?