I'm creating an object to store a bunch of RGB colors, and nesting is allowed. So when looping through the object, I need to see which keys correspond to RGB values or to an object. However, every type guard I've tried won't actually narrow down the type.
type Color = [number, number, number] | 'transparent'
type ColorGroup = Record<string, Color>
type Colors = Record<string, Color | ColorGroup>
const colors: Colors = {
black: [0, 0, 0],
white: [255, 255, 255],
transparent: 'transparent',
primary: {
'50': [211, 233, 252],
'100': [179, 213, 248],
'200': [127, 185, 251],
'300': [68, 156, 253],
'400': [0, 126, 254],
'500': [13, 100, 226],
'600': [17, 79, 189],
'700': [15, 62, 157],
'800': [10, 46, 122],
'900': [1, 22, 77],
}
}
const isColor = (color: Color | ColorGroup): color is Color => {
return Array.isArray(color) || typeof color === 'string'
}
const usesColor = (color: Color):void => {
// does something with the color
}
for(const color in colors) {
if(isColor(colors[color])) usesColor(colors[color]) // error: type 'Record<string, Color>' is not assignable to type 'Color'
}
Any ideas? Am I just missing something fundamental about type guards?
You've run into a design limitation in TypeScript. See microsoft/TypeScript#33391 and microsoft/TypeScript#31445 for more information.
The issue is that the compiler does not keep track of the results of property type guards unless those properties are a string literal or a number literal:
and not if it's a value stored in a variable:
When accessing
colors[color]
, the compiler only knows thatcolor
is a variable of typestring
. After the type guard, you accesscolors[color]
again, but the compiler doesn't realize that you checked it before becausecolor
is just somestring
-typed variable to it. In some sense the compiler can't see the difference between your code and this:which would not be a good use of the type guard.
The above linked issues mention that while it would be nice if code like this were supported, it turns out to be very expensive in terms of compiler resources. Keeping track of which variables are used as indices is a lot of extra and almost always unnecessary work. The use case here is apparently not worth it... especially because:
There is a minor refactoring that gives the behavior you're looking for. Instead of doing multiple indexing operations, do a single indexing operation and save it into its own variable, like this:
Since
c
is its own variable, there's no longer any indexing-with-string
to worry about. The compiler can easily use the type guard onc
to narrow the type ofc
. So, you get the behavior you want, at the expense of slightly less idiomatic JavaScript.Playground link to code