TypeScript Version: 3.5.1
Search Terms:
Code
interface T {
str: string;
bool: boolean;
}
const initState: T = {
str: 'date',
bool: true,
};
let k: keyof T;
initState[k] = 'test' as any;
// ~~~~~~~~~~~~ Type 'any' is not assignable to type 'never'.ts(2322)
This seems to happen specifically with boolean
properties. If I get rid of the boolean
-valued property from the interface it works fine. If I have a larger interface and select non-boolean
properties, it also works fine. But boolean
and another type produces this error.
This error happens with TS 3.5 but not 3.4 (see playground link).
Expected behavior:
No error.
Actual behavior:
Type 'any' is not assignable to type 'never'
Playground Link: link
Related Issues:
This is a documented breaking change; see "Fixes to unsound writes to indexed access types" in the release notes https://devblogs.microsoft.com/typescript/announcing-typescript-3-5/
Thanks @RyanCavanaugh. But I don't follow why this is unsound. k
is keyof T
so it should be a valid index. I can use it to read from initState
just fine:
const v = initState[k]; // ok
The right-hand side has an any
type, so it should be fine to assign to any property in T
.
Just to elaborate on why this seems odd... changing boolean
to number
in the interface makes the error go away, which doesn't seem like it should affect the soundness of the assignment.
interface T {
str: string;
num: number;
}
const initState: T = {
str: 'date',
num: 2,
};
let k: keyof T;
initState[k] = 'test' as any; // ok
I guess it comes down to this, which also doesn't make a ton of sense to me (also in earlier TS versions):
type T1 = number & string; // number & string
type T2 = string & boolean; // never
The break is desired but the inconsistency between boolean (which is secretly a union) and number is not.
This has caused a very large number of errors for us with insufficient guidance of how to fix (from the release notes)...
Most instances of this error represent potential errors in the relevant code. If you are convinced that you are not dealing with an error, you can use a type assertion instead.
OK, so we have _large_ numbers of a pattern that looks like (from the OP):
initState[k] = v; // where k: keyof T and v: T[k]
And these _all_ fail because we have lots of boolean
properties, and so TypeScript thinks the type of initState[k]
is never
(surely that's wrong?)
Is the solution:
boolean
from all interfaces? What should be used instead? Is boolean
deprecated now?k
is boolean
that then gates it out of the initState[k]
check?keyof
to just deal with any
instead and lose type checking? Something like... (initState as any)[k] = v;
@KeithHenry the gist of the breaking change was that you never had type safety in the assignment to begin with. If you want the old behavior, you can put an as any
around the initState[k]
:
interface T {
str: string;
bool: boolean;
}
const initState: T = {
str: 'date',
bool: true,
};
declare let k: keyof T;
initState[k] = 'test'; // Type '"test"' is not assignable to type 'never'.
initState[k] = 'test' as any; // Type 'any' is not assignable to type 'never'.
(initState[k] as any) = 'test'; // ok
Alternatively, you can construct a narrower type than keyof T
which consists of only those keys whose values are assignable to boolean. You can do this quite generally with a conditional type:
interface T {
str: string;
bool: boolean;
bool2: boolean;
d: Date;
}
type KeysOfType<T, U> = { [k in keyof T]: T[k] extends U ? k : never }[keyof T];
const initState: T = {
str: 'date',
bool: true,
bool2: false,
d: new Date(),
};
declare let k: KeysOfType<T, boolean>; // let k: "bool" | "bool2"
initState[k] = 'test'; // Type '"test"' is not assignable to type 'boolean'.
initState[k] = true; // ok
@danvk Thanks for the clarification, that helps.
Our current solution is to replace all initState[k] = 'test'
with (initState as any)[k] = 'test'
, it sounds like that's the correct practice - we can then use KeysOfType
where we need to check.
@KeithHenry if you put the as any
after the property access, TypeScript will check that k
is a valid key. This is more similar to what TS did before 3.5.
(initState[k] as any) = 'test'; // index is checked
(initState as any)[k] = 'test'; // index is not checked
@danvk Cheers, thanks for the clarification!
@danvk
type KeysOfType<T, U> = { [k in keyof T]: T[k] extends U ? k : never }[keyof T];
The type returned by this is a union of keys of T
, but it ends with | undefined
which is not a valid index key.
Any recommendations to avoid undefined
being part of the returned keys union?
Below is a simplified example of my code which suffers from undefined
being in the list of keys.
```ts
class LargeType extends SomeOtherType {
// ... SomeOtherType includes other types of mostly optional properties (number, boolean, etc...)
// and a couple conversion methods that take no parameters but return objects.
prop1?: string,
prop1Op?: myEnum,
prop2?: string,
prop2Op?: myEnum,
// ... long list of similar property pairs
}
myFunction(
myValue: any,
propKey: KeysOfType
myObject: LargeType
) {
if (typeof(myValue) === 'string') {
myObject[propKey] = myValue;
// ^^^^^^^ Type 'undefined' cannot be used as an index type.
}
}
@Zarepheth
Try this:
type KeysOfType<T, U> = { [k in keyof T]-?: T[k] extends U ? k : never }[keyof T];
(I added a -?
in the mapped type to remove the optional-ness of the properties.)
@danvk Thanks! That works!
I'm running into the issue with a Promise that should return a True. This seems like a bug in Typescript. I literally can't set Boolean as a type.
Most helpful comment
@KeithHenry if you put the
as any
after the property access, TypeScript will check thatk
is a valid key. This is more similar to what TS did before 3.5.