Typescript claims it doesn't match the type definition because true
is not assignable to the type string|false
. This only seems to occur in the catch of a promise.
Why does the type widen to boolean only in the catch of the async example?
When returning false as false
from the async function's catch
method, this makes the error go away. It is not necessary to write as false
in the then()
method, only the catch()
, why?
const log = (...data:(
|string
|Error
)[]):Promise<string|false> => {
return fetch('/logs', {
method: 'POST',
body: 'minimal reproducible example',
})
.then(r=>{
if(!r.ok){
return false
}else{
return 'stuff and things'
}
})
.catch(e=>{
return false // `as false` resolves the warning.
})
.finally(()=>{
console.log('ok')
})
}
Type 'Promise<boolean | "stuff and things">' is not assignable to type 'Promise<string | false>'.
Type 'boolean | "stuff and things"' is not assignable to type 'string | false'.
Type 'true' is not assignable to type 'string | false'.(2322)
No quick fixes available
A non async catch returning false does not widen to true|false:
// No warnings:
const log = (...data:(
|string
|Error
)[]):string|false => {
try{
if(Math.random()>.5){
throw 'Error'
}
return 'stuff'
}catch(e){
return false
}
}
The values true
and false
in TypeScript have corresponding literal types also called true
and false
. But quite often when people write true
and assign it to a variable or return it from a function, they intend for the type of that variable or the return type of the function to be the boolean
type. So usually, TypeScript's inference algorithm will widen true
or false
to boolean
(which is actually defined as the union true | false
). But sometimes this isn't what people want, and TypeScript uses some heuristic rules to determine when true
/false
or other literals should remain narrow. If the value is in a context that includes boolean
or true
or false
, then boolean literals tend to remain narrow. Details are documented in microsoft/TypeScript#10676.
Generally speaking when you have a situation where a type is undesirably widened, it's more important to fix it than to know exactly why. The fix for literal values is often to use a const
assertion, since false as const
is explicitly providing the context for narrower types.
But here you're asking why, so let's go through the cases.
In the callback for then()
you've got
{
if (!r.ok) {
return false
} else {
return 'stuff and things'
}
}
which, according to microsoft/TypeScript#10676, gets a union of literal types:
- In a function with no return type annotation and multiple
return
statements, the inferred return type is a union of the return expression types.- In a function with no return type annotation, if the inferred return type is a literal type (but not a literal union type) and the function does not have a contextual type with a return type that includes literal types, the return type is widened to its widened literal type.
It's a union because there are multiple return
statements, and it's unwidened because literal union types are not widened.
In the callback for catch()
you've got
e => { return false }
which by the last bullet point above, is widened from false
to boolean
.
Finally in your synchronous example, you have annotated the function as returning string | false
, so each return
statement is checked against that. But if you left off the annotation, you'd get a union of literal types again:
const log2 = (...data: (string | Error)[]) => {
try {
if (Math.random() > .5) {
throw 'Error'
}
return 'stuff'
} catch (e) {
return false
}
}
// const log2: (...data: (string | Error)[]) => false | "stuff"
So that's "why" it happens, at least to the extent that it is documented to behave that way. As for why the rules were chosen to behave that way, that's not something I can answer except that evidently it meets people's needs in a wide range of real world use cases, and for those cases where it doesn't, you can use const
assertions or other techniques to control the types.