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:
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
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.
Most helpful comment
The first call that assigns to
isUnknownlacks a contextual type to instantiate the parameter so you get back unknown. At that point the parameters are fixed and the assignment toisStringwill fail.The second call is contextually typed by the type annotation on
isString, so the type parameter is inferred to bestring.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.