question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Narrow type of variable when declared as literal (also tuples)

See original GitHub issue

TypeScript Version: 2.4.1

Code

The following doesn’t compile, because x has inferred type string. I think it would be helpful if it did, but I still want x to have inferred type string:

function blah(arg: "foo" | "bar") {
}
let x = "foo";
blah(x);
x = "something else";

Desired behavior:

This would compile.

Actual behavior:

demo.ts(4,6): error TS2345: Argument of type 'string' is not assignable to parameter of type '"foo" | "bar"'.

Suggestion

The type of x should be inferred to be string, but it should be narrowed to "foo" via flow-sensitive typing, much like the below program:

function blah(arg: "foo" | "bar") {
}
let x = "foo";
if (x != "foo") throw "impossible"; // NOTE: codegen would NOT insert this
blah(x);
x = "something else";

Note that this program currently works as desired: x is still inferred to have type string, but the guard immediately narrows it to type "foo". The assignment after the function call widens x’s type back to string.

My suggestion is for TS to treat declarations that assign variables to literals (as in the first example) to automatically perform this flow-sensitive type narrowing, without the need for an unnecessary guard.

I’m suggesting it only for declarations, not general assignments (so only with let, var, and const) or other operations. In addition, I’m only suggesting it for assignments to literal values (literal strings or literal numbers) without function calls, operations, or casts.

Syntax Changes

Nothing changes in syntax.

Code Generation

Nothing changes in code generation.

Semantic Changes

Additional flow-sensitive type narrowing occurs when variables are declared as literals, essentially making the existing

let x = "foo";

behave the same as (in checking, but not in codegen)

let x = "foo";
if (x != "foo") throw "unreachable";

Reverse Compatibility

Due to x being assigned a more-precise type than it was in the past, some code is now marked as unreachable/invalid that previously passed:

let x = "foo";
if (x == "bar") { // error TS2365: Operator '==' cannot be applied to types '"foo"' and '"blah"'.
  // ...
}

The error is correct (in that the comparison could never succeed) but it’s still possibly undesirable.

I don’t know how commonly this occurs in production code for it to be a problem. If it is common, this change could be put behind a flag, or there could be adjustments made to these error cases so that the narrowing doesn’t occur (or still narrows, but won’t cause complaints from the compiler in these error cases).

If the existing behavior is strongly needed, then the change can be circumvented by using an as cast or an indirection through a function, although these are a little awkward:

let x = "foo" as string;
let x = (() => "foo")()

Tuples and Object Literals

It would be helpful if this extended also to literal objects or literal arrays (as tuples).

For example, the following could compile:

function blah(arg: [number, number]) {
}
let x = [1, 3];
blah(x);
x = [1, 2, 3];

with x again having inferred type number[] but being flow-typed to [number, number]. The same basic considerations apply here as above.

Similarly, it could be useful for objects to have similar flow-typing, although I’m not sure if this introduces new soundness holes:

function get(): number {
    return 0;
}
function blah(arg: {a: "foo" | "bar", b: number}) {
    // (...)
}

let y = get(); // y: number
let x = {a: "foo", b: y}; // x: {a: string, b: number}
// x is narrowed to {a: "foo", b: number}

blah(x); // this compiles due to x's narrowed type

x.a = "something else"; // this is accepted, because x: {a: string, b: number}.

See https://github.com/Microsoft/TypeScript/issues/16276 and https://github.com/Microsoft/TypeScript/issues/16360 and probably others for related but different approaches to take here.

Issue Analytics

  • State:open
  • Created 6 years ago
  • Reactions:22
  • Comments:6 (3 by maintainers)

github_iconTop GitHub Comments

3reactions
LinusUcommented, Aug 13, 2018

The more I think about this the more I think it would be highly desirable. I cannot think of any case that would be negatively impacted by making literals be a more narrow type since you can always widen the type. But I can see a lot of positive changes coming from this since you cannot automatically narrow a wide type.

Consider these examples:

declare interface Animal { type: 'cat' | 'dog' }
declare function interactWith (animal: Animal): void

const charlie = { type: 'dog' }

// Argument of { type: string } is not assignable to Animal
interactWith(charlie)

A fix in this simple case would be to add e.g. as Animal to const charlie = ....

type Schema = NullSchema | BooleanSchema | NumberSchema | StringSchema | ArraySchema | ObjectSchema
interface NullSchema { type: 'null' }
interface BooleanSchema { type: 'boolean' }
interface NumberSchema { type: 'number' }
interface StringSchema { type: 'string' }
interface ArraySchema { type: 'array', items: Schema }
interface ObjectSchema { type: 'object', properties: Record<string, Schema> }

declare function validate (schema: Schema, input: unknown): boolean

const schema = {
  type: 'object'
  properties: {
    name: { type: 'string' }
    items: {
      type: 'array',
      items: {
        type: 'object',
        properties: {
          type: { type: 'string' }
        }
      }
    }
  }
}

// Argument of ... is not assignable to Schema
validate(schema, null)

Here the fix is quite annoying, you need to go in and declare the type of every nested schema by making all strings string literals.

const schema = {
  type: 'object' as 'object'
  properties: {
    name: { type: 'string' as 'string' }
    items: {
      type: 'array' as 'array',
      items: {
        type: 'object' as 'object',
        properties: {
          type: { type: 'string' as 'string' }
        }
      }
    }
  }
}

I would love it if we could move forward on this. Is there something that I could contribute with? Do we need to do some kind of research on how this would affect current projects using TypeScript?

2reactions
JCMaiscommented, Nov 8, 2019

For Tuples the way I’m doing it currently is by using as const alongside a Writeable helper to remove the readonly modifier, something like this:

const items = [1, 2, 3] as const

function sum3Numbers(a: [number, number, number]) {
    const [n1, n2, n3] = a
    return n1 + n2 + n3
}

// should give an error
sum3Numbers(items)

// workaround:
type Writeable<T> = { -readonly [P in keyof T]: T[P] }
sum3Numbers(items as Writeable<typeof items>)

Playground Link

Read more comments on GitHub >

github_iconTop Results From Across the Web

Literal types and Enums - mypy 0.991 documentation
Literal types let you indicate that an expression is equal to some specific primitive value. For example, if we annotate a variable with...
Read more >
Documentation - Narrowing - TypeScript
The in operator narrowing​​ For example, with the code: "value" in x . where "value" is a string literal and x is a...
Read more >
typing — Support for type hints — Python 3.11.1 documentation
Tuple [int, float, str] is a tuple of an int, a float and a string. To specify a variable-length tuple of homogeneous type,...
Read more >
TypeScript 3.0: Exploring Tuples and the Unknown Type - Auth0
// Declare the tuple let option: [string, boolean]; // Correctly initialize it option = ["uppercase", true];. If we change value of option to...
Read more >
Crazy, Powerful TypeScript Tuple Types | by Eamonn Boyle
A Tuple (rhymes with 'couple', not 'pupil') is a simple container of data. A tuple object has a fixed size, and the type...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found