Typescript: Consider allowing users to check for uninitialized values

Created on 8 Jul 2016  路  13Comments  路  Source: microsoft/TypeScript

function findMax(nums: number[]) {
    let max: number;

    for (let n of nums) {
        // Error: Variable 'max' is used before being assigned.
        if (max === undefined || n > max) {
            max = n;
        }
    }

    return max;
}

It would be really great if we could let users write this without needing to change max's type to number | undefined.

Working as Intended

Most helpful comment

here is a better sample;

function test () {
    let max: number;
    if (max === undefined) {
        // max used before assinged
        max = 0;
    }
    return max;
}

All 13 comments

here is a better sample;

function test () {
    let max: number;
    if (max === undefined) {
        // max used before assinged
        max = 0;
    }
    return max;
}

@DanielRosenwasser findMax can return undefined, e.g. for findMax([]), so you'd still want the return type to be inferred as number | undefined right?

Aside from that, it would be useful if checking T === undefined didn't trigger the 'used before assigned' error, whatever T is.

@yortus yes, for exactly that reason.

@DanielRosenwasser OK got it. So is this two suggestions in one? One suggestion is to allow some things like max === undefined and return max, even before max is assigned.

The other is that the return type of the following function would be inferred as number | undefined:

function foo() {
    let n: number;
    return n;
}

...which would involve CFA doing a sort of un-narrowing to make the type of n on the last line wider than its declared type.

If this passes through, maybe max == null should be allowed as well.

In my opinion that's a confusing feature. I adds load on the reader of the function and it actually breaks the good pattern of initializing variables before using them. If using JavaScripts default was you intention, why not acknowledge it in the type via T|undefined. It would truly help the reader/maintainer, which is the aspect code should be optimised for.

As a side note, it also adds special casing to the compiler. And what will the IDE tooltips be?

I agree with @gcnew re optimising for readability. Under this suggestion, the line let max: number; does two contradictory things: (1) initializes max to undefined and (2) declares that max cannot be undefined.

That's to save having to write "| undefined" just once, but the code will be read many times. Is there some other advantage here apart from the saved keystrokes?

@yortus @gcnew good point. At the very least, it's questionable whether using an uninitialized value in a return statement is okay.

Agree with the comments above. This doesn't seem like something to bend the design for.

I ran in to this in a case where the initial value comes from a callback, and there is no reasonable default.

I resolved it by using this pattern:

class Example {
    _x: Magic;
    constructor() {
        let __x: Magic | undefined;
        callbackForX( (x:Magic) => {
            __x = x;
        }
        if( ! __x ) { throw new Error( `Missing X!` ); }
        this._x = __x;  //  Typescript knows __x is Magic
    }
}

(Which, of course, only works because the callback is synchronous.)

const myArray = ['foo', 'bar'];
let val: number;
for (let i = 0; i < myArray.length; i++) {
    // There is an if statement here in my code, but it makes no difference. Without it I have the same issue.
    val = i;
}

if (!val) { return; } // Error: Value is used before being assigned

So from what I gather from this discussion, I'm screwed, no workaround? Even though it's TS which is not being logical?

Yeah, this was a headache when I realized that undefined has to be part of your type or you have to do all your existence checking at compile time. Especially when you run into the case 'Type A | null/undefined' can't be assigned to 'Type A | Type B'

// TAKE 1

// someVar is type A | Type B
// we need a way to only use is as type A if it is type A

let someVar: typeA | typeB = foobar
let myVar: typeA | null

if ('ATypeProp' in someVar) {
// FAIL! 'typeA | typeB' not assignable to 'typeA | null'!
myVar = someVar
}

if (typeA) ...use object as type A...

// TAKE 2

let someVar: typeA | typeB = foobar
let myVar: typeA

if ('ATypeProp' in someVar) {
// works, since we know someVar is typeA
myVar = someVar
}

// FAIL! variable being used before assigned
if (typeA) ...use object as type A...

// TAKE 3

let someVar: typeA | typeB = foobar
let isA = 'ATypeProp' in somevar

// FAIL! TS can't infer that isA boolean actually reflects someVars' type-A-ness
if (isA) somevar.ATypeProp

// TAKE 4
let someVar: typeA | typeB = foobar

// everywhere I need to use typeA, annoying and terribly un-DRY
if('typeAProp' in somevar) somevar.ATypeProp

It took me a while to realize that there was no way to do this except for writing out the full conditional every time I use it.

Yeah, this was a headache when I realized that undefined has to be part of your type or you have to do all your existence checking at compile time. Especially when you run into the case 'Type A | null/undefined' can't be assigned to 'Type A | Type B'
...
It took me a while to realize that there was no way to do this except for writing out the full conditional every time I use it.

@ansorensen Perhaps I've misunderstood the problem, but this works for me on TS v3.9.2 (with all strict options):

interface TypeA {
    ATypeProp: string;
}

interface TypeB {
    BTypeProp: string;
}

let someVar: TypeA | TypeB = { ATypeProp: '12345' } as any;
let myVar: TypeA | undefined;

if ('ATypeProp' in someVar) {
    myVar = someVar
}

if (myVar) {
    console.log(myVar.ATypeProp);
}

TypeScript Playground link: https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgCoE8AOECCyDeAUMicjhtgApQD2mAXMgM5hSgDmA3IQL6GGhIsRCgoQAQgWKlxY6nUYs2ILr34AbCGGY0AthABqcKIzF4APmiwTkAXgJk5tBsgDkARgBMAZgAsAVldkHmQ4JlCQdG5NbV10IxMrbAtkAFcQABMIGFAIDO4BGGQACldya3lMINAdfQSASilSZDiEu1rDYzVCktbjRqJmhBoQJhpNADp1GnZivqgJ8qpneu4eIA

Notes:

  1. I needed to change myVar: TypeA | null to myVar: TypeA | undefined to avoid a "Variable 'myVar' is used before being assigned." error in the if (myVar) check. Explicitly assigning myVar to null by default also worked.
  2. The as any cast is needed for this example, otherwise TypeScript automatically infers the type of someVar is TypeA at assignment.

More generally, user-defined type guards are a solution to TypeScript being unable to infer a type correctly. E.g. the error "typeA | typeB' not assignable to 'typeA | null'", to me, indicates TypeScript was unable to automatically narrow the type based on the conditional statement of 'ATypeProp' in someVar.

See https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards for details.

const myArray = ['foo', 'bar'];
let val: number;
for (let i = 0; i < myArray.length; i++) {
  // There is an if statement here in my code, but it makes no difference. Without it I have the same issue.
  val = i;
}

if (!val) { return; } // Error: Value is used before being assigned

So from what I gather from this discussion, I'm screwed, no workaround? Even though it's TS which is not being logical?

@paddotk The workaround is to explicitly declare the type of val as number | undefined.

TypeScript Playground Link

Was this page helpful?
0 / 5 - 0 ratings

Related issues

siddjain picture siddjain  路  3Comments

dlaberge picture dlaberge  路  3Comments

kyasbal-1994 picture kyasbal-1994  路  3Comments

Antony-Jones picture Antony-Jones  路  3Comments

manekinekko picture manekinekko  路  3Comments