Reputation: 5935
I would like to write a type-safe getter that may also return some modified values (but is still sound, I think).
class Foo {
a: number;
b: boolean;
}
function getProp<K extends keyof Foo>(o: Foo, p: K): Foo[K] {
switch (p) {
case 'a':
return 1;
case 'b':
return o[p];
}
return undefined;
}
In the code above, I want getProp
to return Foo[K]
so that in const v = getProp(f, 'a')
v
is of type number
. However, the code throws an error where I return 1
because it cannot be assigned to never
.
Note that this worked in Typescript 3.4 because of "Fixes to unsound writes to indexed access types" (https://devblogs.microsoft.com/typescript/announcing-typescript-3-5/).
How do I best write this code but not write return 1 as any
?
Upvotes: 0
Views: 378
Reputation: 327594
Yes, this issue has been reported fairly often since the release of TypeScript 3.5. As you noted, the improved soundness of indexed accesses is a breaking change that catches a lot of actual bugs but unfortunately also warns on fairly safe code like what you're doing.
At this point the underlying issue is that generic type parameters extending unions don't get narrowed via control flow analysis (see microsoft/TypeScript#24085). There's no way for the compiler to say that since p
can be narrowed to "a"
, that the type parameter K
should be narrowed to "a"
. In general it shouldn't be allowed to do that, since K
could always be "a" | "b"
. The language is currently missing some features that would allow this to happen safely (e.g., if K
could be said not to extends "a" | "b"
but instead something like extends_oneof
"a" | "b"
so that it must be either "a"
or "b"
but not "a" | "b"
, that would help).
For now then there are workarounds and refactoring. The answer given by @TitianCernicova-Dragomir is one possible workaound, using a single-call-signature overload function to essentially revert the behavior inside the implementation back to the checking done in TS3.4 and below. Yes, it's not safe; that's what TS3.5 fixed. Other workarounds involving type assertions or other unsound behavior should also work.
The only reason I wanted to add this answer (other than providing the links above) was to mention a refactoring that could help, depending on your use case. The idea is to forget about control flow narrowing at all, since the compiler can't do it for generics. Instead, you should return something the compiler can recognize as a legitimate Foo[K]
, by indexing into an object of type Foo
with a key of type K
. After all, indexing into an object is, if you squint at it, like doing a switch
on the key:
function getProp<K extends keyof Foo>(o: Foo, p: K): Foo[K] {
return { a: 1, b: o.b }[p];
}
Upvotes: 6
Reputation: 249466
Don't think there is a way to write this in a type-safe way (but if anyone has any stokes of genius I am more than willing to delete this answer). Since p is of type K
it will never be narrowed since it is a union (even though it extends a union). Also you can't assign to Foo[K]
a concrete value, since it still contains the unresolved generic parameter K
.
The one way to do this, which is just a bit better than an as any
is to use a separate implementation signature:
class Foo {
a!: number;
b!: boolean;
}
function getProp<K extends keyof Foo>(o: Foo, p: K): Foo[K] | undefined
function getProp(o: Foo, p: keyof Foo): Foo[keyof Foo] | undefined {
switch (p) {
case 'a':
return 1;
case 'b':
return o[p];
}
return undefined;
}
Upvotes: 1