Skip to content
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

Type incorrectly inferred as "string" rather than String Literal #22038

Closed
benny-medflyt opened this issue Feb 19, 2018 · 15 comments
Closed

Type incorrectly inferred as "string" rather than String Literal #22038

benny-medflyt opened this issue Feb 19, 2018 · 15 comments
Labels
Duplicate An existing issue was already created

Comments

@benny-medflyt
Copy link

Tested with:
TypeScript 2.6.2
TypeScript 2.7.2

Attached is an example where a string literal ("lastName") is being inferred as "string" rather than as the string literal "lastName". Adding a type assertion ("lastName" as any) causes compilation to succeed.

typescript_bug

class Q<s> {
    protected dummy: [Q<s>, s];
}

type MakeCols<s, T extends object> = {
    [P in keyof T]: Col<s, T[P]>;
};

class Col<s, a> {
    protected dummy: [Col<s, a>, s, a];
}

interface Link<Y extends object> {
    c: keyof Y;
}

interface ColDef<a extends object> {
    displayName: string | null;
    link: Link<a> | null;
}

type TableViewCells<T extends object> = {
    [P in keyof T]: ColDef<T>;
};

function declareTableViewThunk<a extends object>(query: <s>(q: Q<s>) => MakeCols<s, a>, colCells: () => TableViewCells<a>): void {
    query;
    colCells;
    throw new Error();
}

function personQuery<s>(q: Q<s>): {
    personId: Col<s, number>;
    lastName: Col<s, string | null>;
    firstName: Col<s, string>;
} {
    q;
    throw new Error();
}

export const agencyTableView = declareTableViewThunk(
    personQuery,
    () => ({
        personId: {
            displayName: "ID",
            link: {
                c: "lastName"
            }
        },
        lastName: {
            displayName: "Last Name",
            link: null
        },
        firstName: {
            displayName: "First Name",
            link: null
        }
    })
);
test.ts(43,5): error TS2345: Argument of type '() => { personId: { displayName: string; link: { c: string; }; }; lastName: { displayName: string...' is not assignable to parameter of type '() => TableViewCells<{ personId: number; lastName: string | null; firstName: string; }>'.
  Type '{ personId: { displayName: string; link: { c: string; }; }; lastName: { displayName: string; link...' is not assignable to type 'TableViewCells<{ personId: number; lastName: string | null; firstName: string; }>'.
    Types of property 'personId' are incompatible.
      Type '{ displayName: string; link: { c: string; }; }' is not assignable to type 'ColDef<{ personId: number; lastName: string | null; firstName: string; }>'.
        Types of property 'link' are incompatible.
          Type '{ c: string; }' is not assignable to type 'Link<{ personId: number; lastName: string | null; firstName: string; }> | null'.
            Type '{ c: string; }' is not assignable to type 'Link<{ personId: number; lastName: string | null; firstName: string; }>'.
              Types of property 'c' are incompatible.
                Type 'string' is not assignable to type '"lastName" | "personId" | "firstName"'.
{
    "compilerOptions": {
        "outDir": "../_build",
        "sourceMap": true,
        "inlineSources": true,
        "declaration": false,
        "removeComments": false,
        "newLine": "LF",
        "preserveConstEnums": false,
        "allowUnreachableCode": false,
        "allowUnusedLabels": false,
        "forceConsistentCasingInFileNames": true,
        "noFallthroughCasesInSwitch": true,
        "noImplicitAny": true,
        "noImplicitReturns": true,
        "noImplicitThis": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "strictNullChecks": true,
        "suppressExcessPropertyErrors": false,
        "suppressImplicitAnyIndexErrors": false,
        "lib": [
            "dom",
            "es2015.collection",
            "es2015.promise",
            "es2015.symbol",
            "es5",
            "es6"
        ],
        "noLib": false,
        "target": "es2015",
        "module": "commonjs",
        "moduleResolution": "node",
        "isolatedModules": false
    }
}
@ghost
Copy link

ghost commented Feb 20, 2018

Could you provide an example that includes an explicit type argument? Type inference is probably not powerful enough to get you a good type here.

@rubenlg
Copy link

rubenlg commented Feb 23, 2018

This bites me quite often too, and it is quite annoying.

function needALiteral(thing: 'THING') { }
const FOO = { thing: 'THING' };
needALiteral(FOO.thing); // ERROR: string is not assignable to 'THING'

Today, the only way to tell Typescript that 'THING' is really meant to be just a literal, is by duplicating it:

function needALiteral(thing: 'THING') { }
const FOO = { thing: 'THING' as 'THING' };
needALiteral(FOO.thing); // Happy compiler

Now, if there is a typo on the first 'THING', it's going to be fun to debug the runtime error.

I wish there was a more compact way to express that, also avoiding the chance of typos causing bugs.

@ghost
Copy link

ghost commented Feb 23, 2018

@rubenlg It's generally a good idea to provide a type to an object literal:

interface I { thing: 'THING' };
const FOO: I = { thing: 'THING' };

That way we give you string literal types and also detect excess properties.

@rubenlg
Copy link

rubenlg commented Feb 23, 2018

@andy-ms I totally agree, and I always do when it's possible/reasonable, but there are situations when it's not. Here is one example:

export interface Foo {
  x: 'FOO' | 'BAR';
  y: number;
  z: {},
}

// private to this module
const commonPart = {
  x: 'FOO',
  y: 2,
};

export const case1: Foo = {
  ...commonPart,
  z: whatever,
};

export const case2: Foo = {
  ...commonPart,
  z: somethingelse,
};

In this case, you don't want to type commonPart. Partial<Foo> is not correct and doesn't work, because it makes everything optional, and the closest match is Pick<Foo, 'x'|'y'> but as you can imagine, it's totally annoying to have to put the keys twice and manually keep them in sync. There are ways around that with high order functions that infer the keys, but that's beside the point. Having a way to tell the compiler "this is a string literal, don't type it as a string" would keep that code simple and fully type checked.

@ghost
Copy link

ghost commented Feb 23, 2018

Now, if there is a typo on the first 'THING', it's going to be fun to debug the runtime error.

Would it help if "apple" as "orange" were a compile error?

@rubenlg
Copy link

rubenlg commented Feb 23, 2018

Totally. Detecting that mistake at compile time would be great.

@ghost
Copy link

ghost commented Feb 23, 2018

OK, created #22150

@gcnew
Copy link
Contributor

gcnew commented Feb 23, 2018

See #14156. I also have a PR implementing it (#14161).

@laughinghan
Copy link

I'm running into the same issue. Minimal test case:

const x = { a: 'abc' };
const y: { a: 'abc' | 'def' } = x; // type error: string not assignable to 'abc' | 'def'

const x2 = { a: 'abc' as 'abc' };
const y2: { a: 'abc' | 'def' } = x2; // typechecks

Try it in the TypeScript Playground

Am I correct in understanding that the reason for this issue is that TypeScript has no way of knowing I don't want to do x.a = 'something else' later? TypeScript can't do that kind of static analysis?

@mhegazy
Copy link
Contributor

mhegazy commented Feb 27, 2018

Duplicate of #20271

@mhegazy mhegazy marked this as a duplicate of #20271 Feb 27, 2018
@mhegazy mhegazy added the Duplicate An existing issue was already created label Feb 27, 2018
@rubenlg
Copy link

rubenlg commented Feb 27, 2018

@mhegazy you closed this as a duplicate of another bug that's also closed. Do I understand correctly that the Typescript team has no intention to fix this problem?

@RyanCavanaugh
Copy link
Member

@rubenlg the specified behavior in this issue is the intended behavior. Some of the repros here represent a non-ideal user experience but there's no sound proposal on the table that is immediately actionable.

@benny-medflyt
Copy link
Author

For those who are stuck, as a last resort you can use this workaround:

function stringLit<a>(val: a): a {
    return val;
}

And then you use it like this:

stringLit<"lastName">("lastName")

This is better than typing "foo" as "foo" since the compiler will verify that you didn't make a typo.

@rubenlg
Copy link

rubenlg commented Mar 5, 2018

That's a great trick @benny-medflyt. Note that it works with numbers too, so you could call it literal.

@typescript-bot
Copy link
Collaborator

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@microsoft microsoft locked and limited conversation to collaborators Jul 25, 2018
Planeshifter referenced this issue in stdlib-js/stdlib Sep 10, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

7 participants