Reputation: 191
As example, I've got a very simple function that sets "true" (boolean) to some named object property:
function setTrue<T, K extends keyof T>(host: T, property: K) {
if (typeof host[property] === 'boolean')
host[property] = true; // <-- TS2322: Type 'true' is not assignable to type 'T[K]'.
}
Can't figure out what's wrong? It seems that type guard typeof host[property] === 'boolean'
doesn't work in this case. Who knows how to achieve correct static type check in this example?
The only workaround I found is if T extends any
but in this case no type guards are required at all:
function setTrue<T extends any, K extends keyof T>(host: T, property: K) {
host[property] = true; // <-- Always Ok
}
Upvotes: 2
Views: 2954
Reputation: 327819
I think there are multiple things contributing to this situation.
One issue is that the compiler does not do type guard narrowing when the property key being tested is a variable; see microsoft/TypeScript#10530 for more information.
Another factor is that a read and subsequent write of a property whose key type is not known to be a singleton cannot be done safely. See microsoft/TypeScript#32693. The compiler doesn't realize that property
is the same exact key in the two lines. All it understands is that the properties being accessed have the same type, which is not strict enough unless the types are already known to be a singleton.
Finally, the compiler just isn't very good at control flow analysis in the face of values that depend on unspecified generic type parameters like T
and K
. Even if you test property
for being a particular subtype of keyof T
, the compiler will not narrow the K
parameter, see microsoft/TypeScript#24085 or microsoft/TypeScript#33014. At best it will narrow the type of property
to some intersection K & ...
which may or may not be helpful.
One caveat to present here before showing workarounds: this isn't technically safe. If your host has a property whose type is known to be the boolean literal false
, as in:
interface Foo {
bar: false
}
declare const foo: Foo;
Then you will run into problems:
setTrue(foo, "bar");
foo.bar; // false or true?
Your implementation of setTrue()
just checks to see if foo.bar
is of boolean
type, which it is. But now we've lied to the compiler by setting a property of type false
to the value true
. So foo.bar
is now true
at runtime but false
in your IDE.
I'm going to ignore this wrinkle, but it's still there, so be careful.
So, workarounds. The easiest by far is just to use a type assertion and move on:
function setTrue<T, K extends keyof T>(host: T, property: K) {
if (typeof host[property] === 'boolean')
(host as any as Record<K, boolean>)[property] = true;
}
Here we've asserted that if host[property]
is of type boolean
, then we will treat host
as Record<K, boolean>
: an object with property values of type boolean
at keys of type K
.
Less straightforward is to refactor into some smaller pieces that the compiler can either verify by itself or that can be made into a user-defined type guard, to walk the compiler through the type analysis the way we want it to:
function setTrueKnownBoolean<K extends PropertyKey>(host: Record<K, boolean>, property: K) {
host[property] = true;
}
function isBooleanProperty<K extends keyof T, T>(
host: T, property: K): host is T & Record<K, boolean> {
return typeof host[property] === "boolean";
}
function setTrue2<K extends keyof T, T>(host: T, property: K) {
if (isBooleanProperty(host, property)) {
setTrueKnownBoolean(host, property);
}
}
The function setTrueKnownBoolean
is only allowed to be called on host
objects which are known to have boolean
properties at the property
key(s), so the implementation needs no type check inside. The function isBooleanProperty
acts as a type guard to narrow the type of host
from T
to T & Record<K, boolean>
. And now setTrue2()
can be implemented by composing those two other functions.
Upvotes: 3