Typescript: Type 'any' is not assignable to type 'never' with boolean in interface in TS 3.5

Created on 29 May 2019  路  15Comments  路  Source: microsoft/TypeScript


TypeScript Version: 3.5.1


Search Terms:

  • Type 'any' is not assignable to type 'never'

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:

Bug

Most helpful comment

@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

All 15 comments

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:

  • Remove boolean from all interfaces? What should be used instead? Is boolean deprecated now?
  • Some way to explicitly handle when the value of property k is boolean that then gates it out of the initState[k] check?
  • Switch all instances that use 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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

zhuravlikjb picture zhuravlikjb  路  3Comments

wmaurer picture wmaurer  路  3Comments

DanielRosenwasser picture DanielRosenwasser  路  3Comments

bgrieder picture bgrieder  路  3Comments

Antony-Jones picture Antony-Jones  路  3Comments