Typescript: "Variable is used before being assigned" even though it is assigned

Created on 18 Dec 2017  路  17Comments  路  Source: microsoft/TypeScript




TypeScript Version: 2.6.1

Code

type Patch<T> = {
  [P in keyof T]?: Patch<T[P]> | ((value: T[P]) => T[P])
}

function applyPatch<T extends object, K extends keyof T>(target: T, patch: Patch<T>): T {
  (Object.keys(patch) as K[]).forEach(key => {
    const patchValue = patch[key];
    target[key] = typeof patchValue == "object" && !Array.isArray(patchValue)
      ? applyPatch(target[key] || {}, patchValue)
      : typeof patchValue == "function" // Error: Variable 'patchValue' is used before being assigned.
        ? patchValue(target[key])
        : patchValue; // Error: Variable 'patchValue' is used before being assigned.
  });
  return target;
}

Expected behavior:
No error

Actual behavior:

Error: Variable 'patchValue' is used before being assigned.

It goes away if I assert patchValue as non-null:

const patchValue = patch[key]!
Bug

Most helpful comment

@markusjohnsson I think that's working as intended because TypeScript still sees the else branch as reachable from a control flow perspective.
It may be useful to use an assertNever helper function:

function assertNever(never: never): never {
    throw new Error(`Did not expect ${never}`);
}

Then return assertNever(a) in the else branch.

All 17 comments

The real error here may be error TS2321: Excessive stack depth comparing types 'T[K]' and 'Patch<T>[K]'., and somehow we forget to mark it as assigned after that.

Oops didn't see the excessive stack error in the IDE

I'm also getting Variable ... is used before being assigned even though it is assigned. Simple repro:

// strict: true in tsconfig

function foo(a: 1 | 2 | 3) {

    if (a == 1) {
        return "a";
    }

    let x: string;

    if (a == 2) {
        x = "b";
    }
    else if (a == 3) {
        x = "c";
    }
    else {
        console.log(a); // a has type 'never'
    }

    return x; // Variable 'x' is used before being assigned
}

@markusjohnsson I think that's working as intended because TypeScript still sees the else branch as reachable from a control flow perspective.
It may be useful to use an assertNever helper function:

function assertNever(never: never): never {
    throw new Error(`Did not expect ${never}`);
}

Then return assertNever(a) in the else branch.

@andy-ms: perhaps, but I get the same error without else (ts 2.7.1, vscode 1.19.3):

// strict: true in tsconfig

function foo(a: 1 | 2 | 3) {

    if (a == 1) {
        return "a";
    }

    let x: string;

    if (a == 2) {
        x = "b";
    }
    else if (a == 3) {
        x = "c";
    }

    return x; // Variable 'x' is used before being assigned
}

That code is equivalent to having else {} -- it's still possible from a control flow perspective (which doesn't look at types) for x to not be assigned.

@andy-ms: I see. Thanks for explaining. You helped me realise that changing else if (a ==3) to just else would probably make this code work (although if valid values were added to the argument type later, it would not introduce an error..).

Is integrating exhaustive type checking into control flow something that would be possible in the future, or would it require too much of a restructure of the compiler?

I don't know how hard that would be, but I've seen issues similar to this come up in the past and be turned down.

What about the non-excessive stack case, where promise is used.
For Example: i'm getting same error here on lineuser.userProfile = profile;

        let status = false;
        let userDao = UserDao.getInstance();
        let profile: UserProfile;
        await userDao.getUserProfile(user.userId).then(pFound =>
            profile = pFound
        );
        user.userProfile = profile;

@ShadabFaiz you could write that as

        let status = false;
        let userDao = UserDao.getInstance();
        let profile: UserProfile;
        let pFound = await userDao.getUserProfile(user.userId);
        profile = pFound;
        user.userProfile = profile;

And the error would go away.

@markusjohnsson I knew about this style and have been using this style for few weeks now, but what i want to understand is that why let pFound = await userDao.getUserProfile(user.userId); works and not

await userDao.getUserProfile(user.userId).then(pFound =>
       profile = pFound
);

@ShadabFaiz well, I don't think that the compiler tracks the then callback and knows that it has been called before the await-statement has returned. Consider this code:

let someThing: Stuff;
button.on('click', () => { someThing = getTheStuff(); });
// should someThing be considered assigned here?

To the compiler, this is (afaik) the same thing.

Noticed the same issue with this code:

const {
    foo = 'bar',
    hello = foo.repeat(2)
} : {
    foo?: string;
    hello?: string;
} = {
    foo: 'world'
}

I think the specification guarantees foo is initialized before hello is deconstructed from the assignment (I'd have to look to be sure), but it definitely ran without error at runtime when I tested it.

Also noticed this issue with:

interface Foo {

}
class Bar {
    constructor(props: Foo) {

    }
}
let props: Foo;
new Bar(props);

the same issue with this code, startTime and endTime variable show error: used before being assigned

export const beforeSubmit = (value: Value): [string, string] => {
  let startTime: string;
  let endTime: string;
  const timeunit = value && value[0];
  if (timeunit === Timeunit.month) {
    const monthTime: any = value[1];
    startTime = `${moment(monthTime).format('YYYY-MM')}-01`;
    const arr = startTime.split('-');
    let month: number | string = Number(arr[1]) + 1;
    month = month < 10 ? `0${month}` : month;
    endTime = arr[1] !== '12' ? `${arr[0]}-${month}-01` : `${Number(arr[0]) + 1}-01-01`;
  }
  if (timeunit === Timeunit.quarter) {
    const quarterTime = value[1];
    startTime = quarterTime as string;
    const arr = startTime.split('-');
    let month: number | string = Number(arr[1]) + 3;
    month = month < 10 ? `0${month}` : month;
    endTime = arr[1] !== '10' ? `${arr[0]}-${month}-01` : `${Number(arr[0]) + 1}-01-01`;
  }
  if (timeunit === Timeunit.year) {
    const yearTime = value[1];
    startTime = yearTime as string;
    const arr4 = startTime.split('-');
    endTime = `${Number(arr4[0]) + 1}-01-01`;
  }
  return [startTime, endTime];
}

then I find a solution, put variables outside:

let startTime: string;
let endTime: string;
export const beforeSubmit = (value: Value): [string, string] => {
  const timeunit = value && value[0];
  if (timeunit === Timeunit.month) {
    const monthTime: any = value[1];
    startTime = `${moment(monthTime).format('YYYY-MM')}-01`;
    const arr = startTime.split('-');
    let month: number | string = Number(arr[1]) + 1;
    month = month < 10 ? `0${month}` : month;
    endTime = arr[1] !== '12' ? `${arr[0]}-${month}-01` : `${Number(arr[0]) + 1}-01-01`;
  }
  if (timeunit === Timeunit.quarter) {
    const quarterTime = value[1];
    startTime = quarterTime as string;
    const arr = startTime.split('-');
    let month: number | string = Number(arr[1]) + 3;
    month = month < 10 ? `0${month}` : month;
    endTime = arr[1] !== '10' ? `${arr[0]}-${month}-01` : `${Number(arr[0]) + 1}-01-01`;
  }
  if (timeunit === Timeunit.year) {
    const yearTime = value[1];
    startTime = yearTime as string;
    const arr4 = startTime.split('-');
    endTime = `${Number(arr4[0]) + 1}-01-01`;
  }
  return [startTime, endTime];
}

but why?

I also have this issue with this code:

let resolver;
new Promise(resolve => (resolver = resolve));
console.log(resolver);

I also have this issue with this code:

let resolver;
new Promise(resolve => (resolver = resolve));
console.log(resolver);

That particular case is a valid warning. Even though we know the executor passed to the Promise constructor is synchronous, TypeScript can't statically determine that it is and assumes that it is asynchronous. If it were hypothetically asynchronous, the variable would indeed be used before being assigned. You can use an assertion ! to hint to TypeScript that you know it's assigned at the point of usage:

let resolver: (value?: unknown) => void;
new Promise(resolve => (resolver = resolve));
console.log(resolver!);

Try it on TS Playground

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jbondc picture jbondc  路  3Comments

DanielRosenwasser picture DanielRosenwasser  路  3Comments

dlaberge picture dlaberge  路  3Comments

bgrieder picture bgrieder  路  3Comments

blendsdk picture blendsdk  路  3Comments