Consider this class:
const creditRiskRatingKeys = [
'applicant_id',
'is_dirty',
] as const;
const dict = {
applicant_id: 'test_id',
is_dirty: true,
}
class CreditRiskRating {
applicant_id: string = '';
is_dirty: boolean = false;
fillQuestionDictionaryToModel() {
for (const key of creditRiskRatingKeys) {
this[key] = dict[key]; // Type 'string | boolean' is not assignable to type 'never'.
}
}
}
Key
is already keyof CreditRiskRating
and this
is referring to the same class. Then why this[key] = dict[key];
should complain?? What am I missing here?
You've basically run into microsoft/TypeScript#32693 as well as microsoft/TypeScript#58905. See those issues for a canonical answer.
An assignment of the form
bar[k] = foo[k]
where Pick<typeof foo, typeof k>
is assignable to Pick<typeof bar, typeof k>
(using the Pick
utility type to say that we only care about the properties of foo
and bar
which exist at keys of type k
) is almost certainly safe. But TypeScript fails to see this in most circumstances.
The first problem happens when k
is of a union type like "applicant_id" | "is_dirty"
. TypeScript only tracks the type of k
, not its identity, so, for all TypeScript knows, you're writing
bar[k2] = foo[k1]
where k2
and k1
are both of the same union type. That wouldn't be safe, so TypeScript complains. The only way an assignment like that could be safe is if the foo[k1]
has a type that works for all possible k2
, which would be the intersection of the relevant property types, not the union:
interface Foo {
a: string,
b: boolean,
}
interface Bar extends Foo {
c: Date
}
function f(foo: Foo, bar: Bar, k: keyof Foo) {
bar[k] = foo[k]; // error!
//~~~~ <-- Type 'string | boolean' is not assignable to type 'never'.
}
This is the subject of microsoft/TypeScript#32693, and it's a longstanding open issue in TypeScript.
TypeScript only wants to allow an assignment to bar[k]
if that value is both a string
and a boolean
. There is no such value... in other words, string & boolean
is the impossible never
type. That's basically the error you're seeing.
The recommended approach is to switch from union-typed keys to generic keys. So instead of k
being of type MyKeys
, you'd have it be of generic type K extends MyKeys
. And if bar
and foo
are identical types, then that works:
function g<K extends keyof Foo>(bar: Foo, foo: Foo, k: K) {
bar[k] = foo[k]; // okay, because bar is a Foo
}
But if the two types are distinct, even if they are equivalent everywhere that matters, then you get an error again:
function h<K extends keyof Foo>(bar: Bar, foo: Foo, k: K) {
bar[k] = foo[k]; // error!
//~~~~ <-- Property 'c' is missing in type 'Foo' but required in type 'Bar'.
}
This is the subject of microsoft/TypeScript#58905, and it's considered a design limitation of TypeScript. The workaround is to synthesize your own types for bar
and foo
such that they are seen as identical. This can happen by just widening them both to the equivalent of Pick
types. Since every Bar
is also a Foo
, we can just widen bar
and leave foo
alone:
function i<K extends keyof Foo>(bar: Bar, foo: Foo, k: K) {
const _bar: Foo = bar; // okay
_bar[k] = foo[k]; // okay
}
So, we need to make things generic, and the object types need to be identical. Doing both with your code looks like:
type CreditRiskRatingKey = typeof creditRiskRatingKeys[number];
class CreditRiskRating {
applicant_id: string = '';
is_dirty: boolean = false;
fillQuestionDictionaryToModel() {
const thiz: typeof dict = this;
creditRiskRatingKeys.forEach(<K extends CreditRiskRatingKey>(key: K) => {
thiz[key] = dict[key];
});
}
}
Here I changed your for...of
loop to a generic callback to forEach()
, and widened this
to typeof dict
before doing the assignment. Now it compiles without error. It's annoying to have to jump through such hoops, but it's the closest we can get to having TypeScript understand our logic. If you don't want to do that you can always use type assertions like this[key] = dict[key] as never
.