Search code examples
typescripttsc

Type inference of differently scoped variables


Consider the code below (running in TypeScript 3.2.2):

const status: 'successful' | 'failed' = 'successful';

function test(): typeof status {
  const status = 'hello';

  return 'successful';
}

This doesn't compile since the return type of test and its signature do not match:

error TS2322: Type '"successful"' is not assignable to type 'IResult<"hello">'.

For some reason the status definition inside the function is being used to determine the return type.

This doesn't happen when using var; this code:

function test(): typeof status {
  var status = 'hello'; // notice the var here

  return 'successful';
}

produces the expected return type of 'successful' | 'failed'.

Using let:

function test(): typeof status {
  let status = 'hello'; // notice the let here

  return 'successful';
} 

This compiles but with the effect that the return type is string, the inner definition is being used again.

I expected tsc to use the status which is defined most high up in scopes to evaluate its return type in both cases, regardless of what declarations exist inside test. Why is the behaviour as observed above? This should be related to how tsc decides which variables to use for type inference.


Solution

  • There are 12 cases here: (const, let, var) * (global, local) * (explicit string union, inferred string)

    TL;DR

    Simple approach:

    • if you specify type explicitly and in the outer scope - result is always the same: union of "ok" | "no".

    Gotcha:

    • because inner let and const ARE available for typeof at return position, they shadow outer declaration ..
    • inner var is not available for typeof at return type position (for whatever reason)

    Add extra confusion:

    • if you try to infer: let and var will consider type to be string, and const will type to exact [immutable original] value.

    List of cases for reference:

    // Infer, Inner
    function test_inner_let(): typeof status01 { // type is string
      let status01 = 'ok'; // let is mutable, so type is only string
      return 'lol';
    }
    function test_inner_const(): typeof status02 { // type is 'ok'
      const status02 = 'ok'; // const allows to specify type to exact 'ok'
      return 'lol'; // error, 'lol' is not assignable to 'ok'
    }
    function test_inner_var(): typeof status03 { // type is any, TS warning: status03 not found
      var status03 = 'ok'; // var is mutable, so type is string
      return 'lol';
    }
    
    // Explicit, Inner
    function test_inner_let_t(): typeof status11 { // type is union 'ok'|'no'
      let status11: 'ok' | 'no' = 'ok';
      return 'lol'; // error, 'lol' is not assignable to 'ok'|'no'
    }
    function test_inner_const_t(): typeof status12 { // type is union 'ok'|'no'
      const status12: 'ok' | 'no' = 'ok';
      return 'lol'; // error, 'lol' is not assignable to 'ok'|'no'
    }
    function test_inner_var_t(): typeof status13 { // type is any, TS warning: status13 not found
      var status13: 'ok' | 'no' = 'ok';
      return 'lol';
    }
    
    // Explicit, Outer - everything works the same
    let status21: 'ok' | 'no' = 'ok';
    function test_outer_let_t(): typeof status21 { // type is union 'ok'|'no'
      return 'lol'; // error, 'lol' is not assignable to 'ok'|'no'
    }
    const status22: 'ok' | 'no' = 'ok';
    function test_outer_const_t(): typeof status22 { // type is union 'ok'|'no'
      return 'lol'; // error, 'lol' is not assignable to 'ok'|'no'
    }
    var status23: 'ok' | 'no' = 'ok';
    function test_outer_var_t(): typeof status23 { // type is union 'ok'|'no'
      return 'lol'; // error, 'lol' is not assignable to 'ok'|'no'
    }
    
    // Infer, Outer
    let status31 = 'ok'; // type is string
    function test_outer_let(): typeof status31 { // type is string
      return 'lol';
    }
    const status32 = 'ok'; // const allows to specify type to exact 'ok'
    function test_outer_const(): typeof status32 { // type is 'ok'
      return 'lol'; // error, 'lol' is not assignable to 'ok'
    }
    var status33 = 'ok'; // var is mutable, so type is string
    function test_outer_var(): typeof status33 { // type is string
      return 'lol';
    }
    
    // (Explicit, Outer const) + (Implicit, Inner)
    const status41: 'ok' | 'no' = 'ok';
    function test_combo_let(): typeof status41 { // type is string, inner let took preference
      let status41 = 'ok';
      return 'lol';
    }
    const status42: 'ok' | 'no' = 'ok';
    function test_combo_const(): typeof status42 { // type is 'sorry', inner const took preference
      const status42 = 'sorry';
      return 'lol'; // error, 'lol' is not assignable to 'sorry'
    }
    const status43: 'ok' | 'no' = 'ok';
    function test_combo_var(): typeof status43 { // type is union 'ok'|'no', var is not bubling up
      var status = 'whatever';
      return 'lol'; // error, 'lol' is not assignable to 'ok'|'no'
    }