Leo Jiang
Leo Jiang

Reputation: 26243

Typescript: checking truthiness of variable doesn't type-guard against undefined?

class Base {}

function log(arg: number) {
    console.log(arg);
}

function fn<T extends typeof Base>(
    instance: Partial<InstanceType<T>>,
    key: keyof InstanceType<T>,
) {
    const val = instance[key];
    if (val) {
        log(val);
    }
}

TS Playground: https://www.typescriptlang.org/play?#code/MYGwhgzhAEBCkFNoG8C+AodAzArgO2ABcBLAez2hFIHMAKMAJ2oC5o8cBbAIwQYEoU6aMOjByEUiAQA6KnUbU+AbnQZs+ImQpY8AHgAq0BAA9CCPABMYhAJ4AHBKSxxEAPlpCRxPBEJgCCKwACowkYCC6AJI+fgH69ggGrq4ANJ7CANYINqxZNk7Q0b7+wAjxDklpAsjpouKE0ABu4dAAvNDexQEA2nkAuioiHc60zSDVtUNyo+HKtRgYQA

I get:

Argument of type 'InstanceType<T>[keyof InstanceType<T>] | undefined' is not assignable to parameter of type 'number'.
  Type 'undefined' is not assignable to type 'number'.

Shouldn't if (val) guard against undefined? It works if I change it to log(val ?? 0).

Also, I'm surprised log(val ?? 0) works. val could be a truthy value that's not a number, but log() expects a number. Why doesn't this throw an error?

Upvotes: 2

Views: 297

Answers (2)

aleksxor
aleksxor

Reputation: 8380

The problem is well known. You may get some insight what's really happening inside the type checker from this comment:

The core problem is that the narrowed type of fooAnchor is not really speakable. Its unnarrowed type is Partial<FooAnchorMap>[keyof TMap], which isn't a union that we can remove undefined and null from to produce some other type that would be provably-assignable to HTMLElement.

There is work in progress to resolve it. But as of now you're left only with workarounds.

function fn<T extends typeof Base>(
    instance: Partial<InstanceType<T>>,
    key: keyof InstanceType<T>,
) {
    const val: InstanceType<T>[keyof InstanceType<T>] | undefined = instance[key];
    if (val) {
        log(val);
    }
}

playground link

And you'll get the correct error: Type 'InstanceType<T>[string]' is not assignable to type 'number'.

Upvotes: 2

nate-kumar
nate-kumar

Reputation: 1771

The undefined bit is a red herring; this is only being flagged because strictNullChecks is enabled which means unless you declare

function log(arg: number) {
    console.log(arg);
}

as

function log(arg: number | undefined) {
    console.log(arg);
}

it is going to complain whenever you pass anything in which may resolve to undefined. Disabling strictNullChecks will eliminate that error (though present others with the above code); alternatively guaranteeing that undefined is handled before being passed in (e.g. ?? 0) will also work.

Also, I'm surprised log(val ?? 0) works. val could be a truthy value that's not a number, but log() expects a number. Why doesn't this throw an error?

This should error if val ?? 0 doesn't resolve to a number and log() expects a number, i.e. in the scenario you described where a truthy non-number is passed in. This fails correctly for me in the playground, do you have an example of this not working?

Upvotes: 0

Related Questions