Skip to content

Index Signature Defined to String but Accept Number #23328

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
MrDesjardins opened this issue Apr 11, 2018 · 10 comments
Closed

Index Signature Defined to String but Accept Number #23328

MrDesjardins opened this issue Apr 11, 2018 · 10 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@MrDesjardins
Copy link

TypeScript Version: 2.8.3-insiders.20180407
(Also in the Playground)

Search Terms: index signature

Code

let x: string = "x";
x = 1; // As expected, this line doesn't compile

interface Obj { 
    [id: string]: boolean;
}
let y: Obj = {};
y["okay"] = true;
y[123] = false; // The id is set to string, why does it compiles?

Expected behavior:
The last line of the code, I would expect to have TypeScript to say the same error as the second line. The error should say that the index must be a string.

Actual behavior:
The code compiles without error even if it is mentioned that only string must be accepted.

Playground Link: https://www.typescriptlang.org/play/index.html#src=let%20x%3A%20string%20%3D%20%22x%22%3B%0Ax%20%3D%201%3B%20%2F%2F%20As%20expected%2C%20this%20line%20doesn't%20compile%0A%0Ainterface%20Obj%20%7B%20%0A%20%20%20%20%5Bid%3A%20string%5D%3A%20boolean%3B%0A%7D%0Alet%20y%3A%20Obj%20%3D%20%7B%7D%3B%0Ay%5B%22okay%22%5D%20%3D%20true%3B%0Ay%5B123%5D%20%3D%20false%3B%20%2F%2F%20The%20id%20is%20set%20to%20string%2C%20why%20does%20it%20compiles%3F

Related Issues: Couldn't find.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Apr 11, 2018
@RyanCavanaugh
Copy link
Member

Please see "Indexable Types" in https://www.typescriptlang.org/docs/handbook/interfaces.html

@MrDesjardins
Copy link
Author

I understand the rationale that JavaScript uses string underneath but it seems that TypeScript, when explicitly set to number, should respect the type. Otherwise, why just not letter developer set it to a number?

Maybe I am missing some context, can you link me to the discussion about why TypeScript is not better than JavaScript in that scenario?

@RyanCavanaugh
Copy link
Member

You can think of numbers as being a subtype of strings for the purposes of indexing. They have predictable and meaningful runtime semantics when used in this way.

It's not clear what error this check would be meant to prevent - if an object is indexable by a string, then writing to it by an implicitly-converted number key is not really problematic.

@MrDesjardins
Copy link
Author

MrDesjardins commented Apr 11, 2018

I was bewildered when I saw a piece of code having IDs (string) used in a situation of index signature with an explicit mention that it should only accept a number. For me, it shatters the idea that TypeScript's explicit typing is always respected.

Let me elaborate. A number is a number in many typed languages, even in TypeScript a variable of type number is not a subtype of string... the index signature exception makes a number a subtype of string weaken the trust of TypeScript's typing. I suggest that we keep it easy and reliable that when something is explicitly mentioned to be a type that this one is enforced like everywhere else in TypeScript. If the index signature can handle both, it seems natural to use a union (string | number) instead of setting it to a number. If the developer really wants to have a number in an index signature that is defined as a string, he should parse the number to be a string -- like anywhere in TypeScript. Maybe I am missing a detail that makes this exception better than follows TypeScript's principles. I am all open to learn about it. In all cases, the single line in the documentation for such a change in mindset doesn't seem to be the greatest way to make something clear from a consumer point of view.

To answer your last sentence, we could also say that there is nothing fundamentally problematic in JavaScript once you master all the quirks. However, TypeScript's leverage is that you can explicitly have a "contract" and expect the code developed to follow it. The index signature exception or quirk that a number can accept a string violates, in my opinion, what the developer wrote and wanted to be: a number.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Apr 11, 2018

If the documentation explain why every decision was made the way it was, it would ten times as big and would take ten times as long to read.

Index signatures predate union types, so [s: string | number]: T was not conceivable when the behavior for index signatures was defined. We thought it was cumbersome/stupid to make you write [s: string]: T and [s: number]: T, especially when "under the hood" everything really is just a string key.

So then the question is, should we have broken everyone using numbers to write to string keys? The default answer to "Should we incur a large breaking change?" is "No". And the default answer is "Should we add a commandline flag for every conceivable permutation of behavior?" is also "No".

The reason we don't let you write code like {} / [1, 2] is that we think those operations, while well-defined, are very likely to be incorrect (i.e. not what you meant to do). Conversely, we think code like "Hello, my age is " + age is not likely to be incorrect. So we have to decide, if you have an object that says it can be indexed by any string, and you try to index that by a number which we know will be safely converted to a string, is that likely to be incorrect? If you had written x[n + ""] = e, we would have allowed it - is this really any different? Why did a no-op change the safety of the code?

Now you might say, that's incorrect logic, because this code is rejected:

function fn(x: string) { const s = x + ""; }
fn(10); // Error

But that's because we don't really know that you didn't do this:

function fn(x: string) { x.toLowerCase() }
fn(10); // Type error here prevents crash

But if there were a type that defined "safely implicitly converts to string":

function fn(x: safe_to_implicit_convert_to_string) { const s = x + ""; }
fn(10); // close enough
fn({}); // probably still wrong

then we could define which types have meaningful implicit string coercions (number, boolean, string, Date) and allow those calls while still disallowing others.

If we had the "safely implicitly converts to string" type, that's the type that would be implied by writing a string index signature:

type t = { [key: safe_to_implicit_convert_to_string]: T };

...but we don't. However, you still do get that behavior with regard to numbers.

@MrDesjardins
Copy link
Author

Thank you, Ryan, for the explanation. Now, I understand the motive of not breaking previous codes, but I am still not entirely sold to the idea of explicitly a type and allowing more.

Concerning the documentation, if a particular case (like this one) is not fully documented, then you may have people asking questions. This thread will act as future documentation for people who are wondering about this quirk. So, great. That being said, I enjoy seeing many decisions discussed in Github's threads which act as "advanced documentation". I initially thought that you might have a link to one about that subject. However, I am pleased with your response. Thanks that you took your time to do it today.

If I may abuse your time a little bit more, I am still questioning why the union definition between the two accepted type (string and number) is illegal syntax in TypeScript:

type MyIndex = { [index:string | number] : Item};

Since a number is safe to be a string, and a string is also legal, why is the union of these two legal primitives is not? I understand that it may be cumbersome since writing a number handles both cases, but the TS compiler error message mentions that it must be a string OR a number and someone that would like to pass both will use the union if he doesn't know he can use a number for the case of number and string. (I am advocating here to keep the magic trick of number to preserve compatibility AND to support union).

In the end, what I understand is that we do not really care about the type of the index signature, neither we care about the name of the index signature. If we think outside the box, we could have a syntax without the name and without a type, and have TypeScript handling any type passed (which would accommodate the option to give a boolean which is legal in JavaScript even if not pragmatic).

type MyIndex = { [] : Item};

@RyanCavanaugh
Copy link
Member

I am still questioning why the union definition between the two accepted type (string and number) is illegal syntax in TypeScript

You have to think about this historically, not from a clean slate. There is path dependence in the world. The decisions made six years ago affect the decisions we make now; you can't just imagine what the perfect syntax/behavior should be today and then assume that any deviation from that implies a bug or thing that ought to exist.

[s: string]: T, since the beginning, has meant "Can be indexed with string or number. Why would we add new syntax to do the same thing? It's extra work to handle this, and would be confusing to have a declaration form that was just a synonym for something else.

Adding the string|number form would imply that the string form did not accept number - but again, breaking changes and path dependence. If you think the behavior as-is is confusing and deserves more documentation, imagine how confusing it would be to if the form you're proposing were legal - the legitimate question would be "Why is there a form that does exactly the same thing but makes it look like the other form has different behavior from what it does?"

@MrDesjardins
Copy link
Author

MrDesjardins commented Apr 11, 2018

Why would we add new syntax to do the same thing?

This is a good question. The current syntax is not naturally logic when we compare to the rest of the language. We already discuss this, and we agree that because of a historical reason it ended in the present form. However, I see it more as a step into cleaning the language. TypeScript could in the future have a warning to deprecate the magic string/number safe conversion and in a farther future removes the warning into a compilation error.

Evolution is normal beyond technology like in speaking languages (e.g ., the English language from 100 years ago is different from now). Past debts are typical, I am not disputing this. However, like any garden, we must groom it and not just plan new flowers. I am 100% behind not having radical changes that break people suddenly. Microsoft, in general, is stellar by having long-term support and having good developer experience by keeping thing functional as possible. Kudos. However, it is sane to have a gradual process to lean into a cleaner state.

Why is there a form that does the same thing but makes it look like the other form has different behavior from what it does?

Many concepts have different in syntax and do the same thing. On the top of my head:

x:number[];
x:Array<number>;

The evolutionary suggestion is not confusing if the warning message specifies the reason. You can even link it to the content we are generating here that provide context. Any rational person will understand that in the past, some features were not there in TypeScript to support the ideal syntax. However, many years later, it is available and to keep TypeScript aligned to be easy and consistent in the language, few minor syntaxes evolve.

Again, thank you very much for your thorough follow-up and to take into consideration my different perspective on the topic.

@AaronJan
Copy link

Is this issue related? #22105

@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 31, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants