Type inference of differently scoped variables

Viewed 96

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.

1 Answers

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'
}
Related