In TypeScript, control flow analysis is used to narrow down the apparent type of an expression and make it more specific. For instance, when a value is assigned to a variable with a union type, the compiler narrows down the variable's type to only those members of the union that are compatible with the assigned value until the next assignment is made. As shown in this example:
let foo: string | null = ''
foo // string
After the assignment, the compiler has narrowed down `foo` from `string | null` to just `string` because it recognizes that `foo` cannot be `null`. However, if you change the assignment to something like `let foo = Math.random()<0.5 ? '' : null`, the narrowing won't occur as expected.
Control flow analysis can either narrow down the type of an expression or reset it back to its original type. It's not feasible to widen a type arbitrarily or mutate it into something unrelated.
When you invoke a user-defined type guard function like:
const isNullableX = (value: any): value is X | null => false
it will narrow down the input type based on the output. In the following call:
if (isNullableX(foo)) {
foo // X
}
`foo` is narrowed down from `string` to something assignable to `X | null`. Since `null` cannot be assigned to `string`, the only possible result here is `X`, leading to the narrowing of `foo` to `X`. If you were expecting `foo` to change from `string` to `X | null`, that cannot happen without a reset followed by a narrowing, which isn't the case here since there is no reassignment involved.
Link to code on Playground