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

String literal that fits keyof X cannot be used as key of X in some cases #32427

Closed
silverwzw opened this issue Jul 16, 2019 · 5 comments
Closed
Labels
Question An issue which isn't directly actionable in code

Comments

@silverwzw
Copy link

silverwzw commented Jul 16, 2019

TypeScript Version: 3.5.3

Search Terms:

  • keyof
  • string literal
  • extends

Code

type Config = { method: "POST" | "GET" };

<X extends Config>() => {
    type Mapper = { [ item in X["method"] ] : number };

    let mapper : Mapper;

    let a = { POST:1, GET:2 }; // `a` has type `{ POST: number, GET: number }`
    mapper = a; //  ✔, tsc think this is OK
    mapper = { POST:1, GET:2 };  //  ❌, but this is not

    let key : keyof Mapper;
    key = "FETCH"; // ❌, tsc think "FETCH" is not a valid key
    key = "POST"; //  ✔, but "POST" is a valid key
    mapper[key] = 0; //  ✔, tsc think mapper[key] is OK
    mapper["POST"] = 0; // ❌, but mapper["POST"] is not
};

Expected behavior:
tsc either reports no syntax error on the above code snippet. Or error on all four lines:

    mapper = a;
    mapper = { POST:1, GET:2 }; 
    mapper[key] = 0;
    mapper["POST"] = 0; 

Actual behavior:
tsc complains on the two marked above with Object literal may only specify known properties, and 'POST' does not exist in type 'Mapper'. But not reporting error on the other 2 lines.

Playground Link:
https://www.typescriptlang.org/play/?target=1#code/C4TwDgpgBAwg9gOwGYEsDmUC8UDeUC2EwAFnACYBcUARAAoDyAygCrVQA+NA4gKKtQBfANwAoEQB4AGlAgAPYBARkAzrESo0APgAUASiybcIqCaihIUALIBDMJABOWXFADaUFAvzuEUSS+qEJOTUALpQYVQIAK74AEYQjsJiplAANkQEtg5QVDZ2CaLGpunAUNZOeAwsFACMADRQvMwUAEyCQlAA9J1QAAbWvVDE1qrm0L2VTM1Q0XEJDU2RMfGJvUUm+FkJTtYd3SaAKOQNwMoAxmbEKAgA1hcoqvdQ9ADS65n5jtiT1fWNfK3tEz7KCAGXIGrEoqUSI9Hgg4MBksUMtcICAclAUSA4EgrFt7KIUpinNQAGJ8GAACWoex6YLMZzuNxoZOYlLYsPhZSgADdrKkUGQMai3kTsHQptSuj0oEcoBDSuKWOzVOVefzBZi3psPi5MWFsAAGGmHY4M6FM7UOXWosKPF5avH+KqsfVQI1S0HgyHvK2Kl3uVRwhHCIA

Related Issues:

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Jul 16, 2019

const f = <X extends Config>() => {
  /* snip */
};
/**
 * Of course you **MUST NOT** set `mapper["POST"]`!
 * Because "POST" is not assignable to `X["method"]`
 */
f<{ method : "GET" }>();

Whatever it is you are trying to do, it is a code smell.
In general, if you have a generic type parameter but no function parameters use the generic type parameter, it's a serious code smell.


Change

mapper = { POST:1, GET:2 };

to

mapper = { GET:2 , POST:1 };

And it will complain about the "GET" now


Change

mapper["POST"] = 0;

to

mapper["GET"] = 0;

And it will complain about the "GET" now

@silverwzw
Copy link
Author

silverwzw commented Jul 16, 2019

@AnyhowStep Thank you for pointing out that { method: "POST" } actually does extend { method: "POST"|"GET" }. I wasn't aware of that. I have edited the original issue description on that.

This is a simplified example, just to illustrate the issue. The actual code that leads me to this is using the type parameter in the parameter list.

Now I think the issue is that:

    let a = { POST:1, GET:2 }; // `a` has type `{ POST: number, GET: number }`
    mapper = a;

and

    mapper = { POST:1, GET:2 };

The two pieces of code shall both fail or pass the syntax check. It's weird that one passes the check while the other failed the check.

Same thing to:

    let key : keyof Mapper = "POST";
    mapper[key] = 0;

v.s.

    mapper["POST"] = 0;

Whats' more, if you do:

    let key : keyof Mapper = "FETCH";

tsc will complain, so tsc think "FETCH" is not a legit key but "POST" is.

@fatcerberus
Copy link

fatcerberus commented Jul 16, 2019

let a = { POST:1, GET:2 }; // `a` has type `{ POST: number, GET: number }`
mapper = a; //  ✔, tsc think this is OK
mapper = { POST:1, GET:2 };  //  ❌, but this is not

This is because of excess property checking, which only applies to "fresh" object literals. Types are not closed, otherwise you wouldn't be able to have subtypes under structural typing.

I still believe the error message for this needs to be reworded because it seems to be very misleading to people as it is right now. See discussion here: #32158 (comment)

@poseidonCore
Copy link

You can rewrite this as

type Config = {method:"POST"|"GET"};
type Mapper = {[item in Config["method"]]:number};

function fn<X extends Config>() {
    let mapper:Mapper;

    let a = {POST: 1, GET: 2}; // `a` has type `{ POST: number, GET: number }`

    mapper = a; //  ✔, tsc think this is OK

    mapper = {POST: 1, GET: 2}; // ✔

    let key: keyof Mapper;
    key = "FETCH"; // ❌, tsc think "FETCH" is not a valid key
    key = "POST"; //  ✔
    mapper[key] = 0; //  ✔, tsc think mapper[key] is OK
    mapper["POST"] = 0; // ✔
}

to overcome some of these issues.

Putting the type declaration inside the function to reference the generic may be problematic here.

TS Playground

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Jul 23, 2019
@typescript-bot
Copy link
Collaborator

This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

6 participants