Typescript: Incompatible type guards are assignable to each other

Created on 5 Aug 2020  Β·  6Comments  Β·  Source: microsoft/TypeScript

TypeScript Version: 4.0.0-beta


Search Terms: type guard, assignability, covariance, strictFunctionTypes

Expected behavior:

isUnknown is not assignable to isString.

Actual behavior:

isUnknown is _sometimes_ assignable to isString. At other times it's not.


Related Issues:

29501

Code

I apologize for using a dependency here, but I wasn't able to reproduce the issue without it.

import Refinement from 'refinements';

/**
 * Let's suppose I want to create a type guard like this.
 */
let isString: (candidate: unknown) => candidate is string;

/**
 * And let's suppose I have a type guard of type `(candidate: unknown) => candidate is unknown`.
 */
const isUnknown = Refinement.create(
  _ => Refinement.miss
);

/**
 * As expected, this doesn't compile.
 */
isString = isUnknown;

/**
 * Unexpectedly, this does β€” even though the right-hand side is of exactly the same type as `isUnknown`.
 */
isString = Refinement.create(
  _ => Refinement.miss
);

Output

let isString;
import Refinement from 'refinements';
// isUnknown is of type `(candidate: unknown) => candidate is unknown`
const isUnknown = Refinement.create(candidate => Refinement.miss);
isString = isUnknown;
isString = Refinement.create(candidate => Refinement.miss);

Compiler Options

{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "alwaysStrict": true,
    "esModuleInterop": true,
    "declaration": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "moduleResolution": 2,
    "target": "ES2017",
    "jsx": "React",
    "module": "ESNext"
  }
}

Playground Link: Provided

Working as Intended

Most helpful comment

The first call that assigns to isUnknown lacks a contextual type to instantiate the parameter so you get back unknown. At that point the parameters are fixed and the assignment to isString will fail.

The second call is contextually typed by the type annotation on isString, so the type parameter is inferred to be string.

I think this comes down to the design whereby type-inference is local to the call site. Contextual typing lets you push information down to the call-site in some cases, but it doesn't extend as far as to propagate back through assignments of returned values. For this to work you would need a global analysis.

Here is a mini example to demonstrate contextual typing, in this case, for inferring literal types.

let obj: 5;
let obj2 = 5
obj = obj2 // error
obj = 5

All 6 comments

Unexpectedly, this does β€” even though the right-hand side is of exactly the same type as isUnknown.

But it's not the same type. If you hover over it you can see that the type is (candidate: unknown) => candidate is string. With the direct assignment the second generic type parameter is inferred to be string, while in your isUnknown case there's no information to infer the type from and it's unknown.

Here's a minimal example without a dependency:

function identity<T>(): T { return {} as any; }

let isString: string;
let isUnknown = identity(); // T inferred to be unknown.
isString = isUnknown;       // unknown is not assignable to string.
isString = identity();      // T is inferred to be string, assigning possible.

The first call that assigns to isUnknown lacks a contextual type to instantiate the parameter so you get back unknown. At that point the parameters are fixed and the assignment to isString will fail.

The second call is contextually typed by the type annotation on isString, so the type parameter is inferred to be string.

I think this comes down to the design whereby type-inference is local to the call site. Contextual typing lets you push information down to the call-site in some cases, but it doesn't extend as far as to propagate back through assignments of returned values. For this to work you would need a global analysis.

Here is a mini example to demonstrate contextual typing, in this case, for inferring literal types.

let obj: 5;
let obj2 = 5
obj = obj2 // error
obj = 5

Good explanation, thank you both.

I suppose there is nothing to do here?

I suppose there is nothing to do here?

Everything is working as intended. :-)

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

fwanicka picture fwanicka  Β·  3Comments

zhuravlikjb picture zhuravlikjb  Β·  3Comments

dlaberge picture dlaberge  Β·  3Comments

blendsdk picture blendsdk  Β·  3Comments

DanielRosenwasser picture DanielRosenwasser  Β·  3Comments