This block of code showcases a function designed for compact, immutable editing of a record to assign boolean values.
The function is meant to take in a record containing boolean values and a list of keys that match. The result should be a new record where all specified keys are set to true, using an 'immutable' update approach that leaves the original record unchanged.
The errors I'm encountering seem quite fundamental, prompting me to seek another perspective. It feels like there must be something essential that I'm overlooking. How can I adjust Generics to ensure the code below compiles and runs correctly?
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 experiment with the issue on this online playground
The problematic line indicates a challenge with Typescript inferring Key
too narrowly. By only inferring from the keys
array instead of also considering the flags
object, it ends up flagging the flags
object as invalid if it contains any property names not in keys
...
MOTIVATING EXAMPLE
While this example is fairly straightforward, a much more intricate scenario I'm dealing with presents a similar issue, where an error condition akin to the excess property checking here arises - meaning that keys
dictates the narrowing type of
flags</code when ideally it should be vice versa - <code>keys
should be deduced from the properties of flags
.
WORKAROUNDS
One might expect the workaround below to establish 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, this approach triggers an even stranger error. Hovering over the seemingly erroneous assignment to the nextFlags property reveals the unexpected error lines (believe it or not)...
const nextFlags: Flags extends Record<Key, boolean>
Type 'boolean' is not assignable to type 'Flags[Key]'
I have also attempted to use keyof
to derive the keys directly from the type of
flags</code, yielding identical results despite completely eliminating the <code>Key
generic and fully deriving the type of keys</code from <code>flags
.
// use keyof to ensure that the property name aligns
function createNextFlags<Flags extends Record<any, boolean>>(
flags: Flags,
...keys: [keyof Flags, ...(keyof Flags)[]]
)
Nevertheless, it produces the same type of error.
const nextFlags: Flags extends Record<any, boolean>
Type 'boolean' is not assignable to type 'Flags[keyof Flags]'
I also experimented with utilizing infer
to extract keys directly from the type of flags
as demonstrated in the following example...
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 puzzling error message...
const nextFlags: Flags extends Record<any, boolean>
Type 'boolean' is not assignable to type 'Flags[InferKey<Flags>]'
What is the correct approach to resolving this problem so that the Key
type can be inferred from the flags
object to constrain the keys
argument? What am I failing to grasp?