Skip to content

Literal type false unexpectedly widens into type boolean #25474

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
cliffkoh opened this issue Jul 5, 2018 · 6 comments · Fixed by #27042
Closed

Literal type false unexpectedly widens into type boolean #25474

cliffkoh opened this issue Jul 5, 2018 · 6 comments · Fixed by #27042
Assignees
Labels
Bug A bug in TypeScript

Comments

@cliffkoh
Copy link

cliffkoh commented Jul 5, 2018

TypeScript Version: 3.0.0-dev.20180705

Search Terms: false/true loosened/expanded/loses specificity into boolean

Code

type Foo = FooBase | FooArray;
type FooBase = string | false;
type FooArray = FooBase[];

declare let foo1: Foo;
declare let foo2: Foo;
foo1 = [...Array.isArray(foo2) ? foo2 : [foo2]];

Expected behavior:
No error.. In the else clause, [foo2], it retains string | false so Array<string | false>. Logically since everything is taking place in the typings layer, foo2 should still be strongly bounded to false and will never be true.

Actual behavior:
Errors. In the else clause, [foo2], it expanded/loosened to string | boolean so Array<string | boolean>.

tmp.ts:7:1 - error TS2322: Type '(string | boolean)[]' is not assignable to type 'Foo'.
  Type '(string | boolean)[]' is not assignable to type 'FooBase[]'.
    Type 'string | boolean' is not assignable to type 'FooBase'.
      Type 'true' is not assignable to type 'FooBase'.

7 foo1 = [...Array.isArray(foo2) ? foo2 : [foo2]];
  ~~~~

Playground Link: Link

Related Issues:

@cliffkoh cliffkoh changed the title Type of false unexpectedly loosens into type boolean Type of false unexpectedly widens into type boolean Jul 5, 2018
@pedro-pedrosa
Copy link

Possibly related to this:

function trySomething(x: number) {
    if (x < 0) {
        return {
            result: false,
            message: 'Negative numbers not allowed'
        };
    }
    return {
        result: true,
    };
}

const a = trySomething(-1);
console.log(a.result ? 'Success' : `Error: ${a.message}`);

The return type of trySomething is { result: boolean; message: string; } | { result: boolean; message?: undefined; } while it should be { result: false; message: string; } | { result: true; message?: undefined; }

If I use strictNullChecks and try to use a.message I will get an error saying it's possibly undefined.

Playground link

@Ghabriel
Copy link

Ghabriel commented Jul 6, 2018

@pedro-pedrosa I don't think it's related, TS always widens literals like that unless you do something like this, which works:

function trySomething(x: number) {
    if (x < 0) {
        return {
            result: false as false,
            message: 'Negative numbers not allowed'
        };
    }
    return {
        result: true as true,
    };
}

const a = trySomething(-1);
console.log(a.result ? 'Success' : `Error: ${a.message}`);

@pedro-pedrosa
Copy link

Okay that probably makes some sense, typescript probably doesn't want to make the type too strict as users of the function may want to write values to the return value of the function.

@cliffkoh
Copy link
Author

cliffkoh commented Jul 6, 2018

Note that there isn't a bug with string literal types, only false/true into boolean

type Foo = FooBase | FooArray;
type FooBase = 'blah' | 'mah';
type FooArray = FooBase[];

declare let foo1: Foo;
declare let foo2: Foo;
foo1 = [...Array.isArray(foo2) ? foo2 : [foo2]];

This works. type of [foo2] in the conditional does not get widened into string | string.

@ahejlsberg
Copy link
Member

This is an issue resulting from two separate factors:

  • We have non-widening forms of the string and numeric literal types (Non-widening explicit literal types #11126), but the true and false literal types and enum literal types are always considered widening.
  • Spread expressions aren't considered to have a contextual type.

Because the array literal [foo2] occurs in a spread expression it isn't contextually typed by FooBase (the type of foo1) and therefore the string | false type is widened to string | boolean. This very similar assignment has no issues because the contextual type propagates appropriately:

declare let fa: Foo[];
declare let foo2: Foo;
fa = Array.isArray(foo2) ? foo2 : [foo2];

To fix this issue we need to propagate contextual types into spread expressions.

@ahejlsberg ahejlsberg added the Bug A bug in TypeScript label Jul 6, 2018
@cliffkoh cliffkoh changed the title Type of false unexpectedly widens into type boolean Literal type false unexpectedly widens into type boolean Jul 6, 2018
@mhegazy mhegazy added this to the TypeScript 3.1 milestone Jul 6, 2018
@CS-Jfilmer
Copy link

Sorry, I feel like the answers I need are in this / #22596 but I can't grok it for the life of me

function x<Arg1, Arg2 extends (Arg1 extends true ? string[] : string)>(arg2IsArray: Arg1, arg2: Arg2){
}

x(true, 'string') // no error
x(true, ['string']) // no error

x(true as true, 'string') // error
x(true as true, ['string']) // no error

Is it that "true" means "boolean" if used in certain contexts to support

const x = true // x is a boolean not literally "true"?

But I just tried that and ts seems to think that x is just true not a boolean, idk. Anyway is there anyway to have the above example work without having to use true as true?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants