Skip to content

New numeric string type to accurately represent numeric indexes #44649

New issue

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

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

Already on GitHub? # to your account

Open
rosenbergd opened this issue Jun 18, 2021 · 16 comments
Open

New numeric string type to accurately represent numeric indexes #44649

rosenbergd opened this issue Jun 18, 2021 · 16 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@rosenbergd
Copy link

rosenbergd commented Jun 18, 2021

Suggestion

πŸ” Search Terms

Object key type, numeric string type, index signature parameter type

βœ… Viability Checklist

My suggestion meets these guidelines:

  • [βœ…] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [βœ…] This wouldn't change the runtime behavior of existing JavaScript code
  • [βœ…] This could be implemented without emitting different JS based on the types of the expressions
  • [βœ…] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • [βœ…] This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

A new type should be created to represent numbers in string or number format (a number can be used where this type is used, but the real type is a string representation of a numerical value).

This should be used in cases where object indexes are set to number to allow for number and string representation of said number to be used in indexing and to make it more accurate as the indexes are actually stored as strings even if the restriction is that the creation of a new index should be done with a number.

This will also help with a few other places where the type is indicated as a number, but the real type is a string.

πŸ“ƒ Motivating Example & πŸ’» Use Cases

Conciseness improvment:

const a: {[prop: number]: number} = {};

a[1] = 123;
// below doesn't currently work even though there is no difference and this means the exact same thing as `a[2] = 321;`
a['2'] = 321; // assuming this worked for the rest of the example below

let i: string = ''; // here to show what the type of `i` is currently
for (i in a) {
    console.log(a, a[i], typeof i);
    // above will outputs { "1": 123, "2": 321 }, 123, "string"
    // in the first iteration, note the difference between the type specified in the object index type (`number`) and the type it really is (`string`)
}

This would instead be something like this (name of type is not final, I'm sure there are better names this can have)

const a: {[prop: numstring]: number} = {};
// since old versions allow `number`, this will be an added possiblity to be more concise
// the type above will make it more obvious that it's a string representation of a number (avoids surprises)

a[1] = 123;
a['2'] = 321; // this would now be allowed as well

let i: numstring = '0'; // here to show what the type of `i` is (type compatible with string, no parsing needed between them)
// can also be `let i: numstring = 0` to auto-wrap numbers
for (i in a) {
    console.log(a, a[i], typeof i);
    // above will outputs: { "1": 123, "2": 321 }, 123, "string"
    // in the first iteration, "string" will be the typeof similarly to how all objects are just "object" with `typeof`

    // this should make it more specific as to what the type is and whether or not the type can be converted to a number

    // the following can also be made to work:
    if (i === '1') {...}
}

This will also be useful in a similar way when looping through an array as the indexes are also treated as strings.

And another benefit outside of this will be to allow for string representation of numbers to be better defined as such in general

@rosenbergd
Copy link
Author

I linked the old issue I created for this even though it's not relevant anymore and should be ignored since it's easier to follow for anyone watching the previous issue

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Jun 18, 2021
@ahejlsberg
Copy link
Member

There is ongoing work to improve index signatures in #44512. That PR includes fixing index signatures with key type number to consistently apply to values assignable to type number as well as all string literal types with round-tripping numeric values. Round-tripping here means that a given string value s satisfies the check +s.toString() === s. So, the a['2'] = 321 assignment in your example above will be permitted once #44512 is merged.

We don't have a predefined type to represent a string with a round-tripping numeric value, but the template literal type `${number}` comes awfully close. It allows any string that can successfully be parsed as a number. That includes non-round-tripping strings like '1.0', '0.00', but I don't think the difference is important enough to merit a whole new type.

@fatcerberus
Copy link

I guess to be 100% correct, only integers should be usable as indices, but there’s probably no way to enforce that at the type level.

@ahejlsberg
Copy link
Member

I guess to be 100% correct, only integers should be usable as indices

Actually, there's nothing in JavaScript's automatic number-to-string conversion when indexing an object that requires integers. It's true there are optimizations for arrays that only apply for integer values, but that's really an orthogonal issue. The thing we're wanting for check for string literals used with numeric index signatures is that obj[s] and obj[+s] access the same property--because if they don't, then the numeric index signature shouldn't apply.

@rosenbergd
Copy link
Author

rosenbergd commented Jun 19, 2021

How much of this code will be possible?

const a: {[prop: `${number}`]: number} = {};

a[1] = 123; // currently not allowed when using the above index type
a['2'] = 321; // also currently not allowed

let i: string = '0';
for (i in a) {
    console.log(a, a[i], typeof i); // first iteration: { "1": 123, "2": 321 }, 123, "string"
    // above `a[i]` doesn't work either
    if (i === '1') {...}
}

It's currently not possible to use the type `${number}` which means that the type of the index can't be specified in such a way that it represents a numerical string index to reflect the true value of i in the loop which is the main point of this feature request, is that planned for that PR as well?

What I'm basically asking for is some way to be more accurate in the type of the indexes in such a way that it represents my intent exactly rather than relying on a developer reading the code understanding that this is how it really behaves, I understand that I can simply use this format above where I split i from the loop so it's immediately obvious, but it still isn't ideal since it looks like the types mismatch and I end up creating an extra variable that might not be needed after the loop.

@fatcerberus
Copy link

The problem with your example is that, if type safety is to be maintained, i in that loop can never be more specific than string, even with the proposed index signature, because there's no guarantee the actual object doesn't have more properties. That's a basic rule of the way TS's structural typing works and can't be changed, and is the same reason why Object.keys() doesn't return a keyof type.

To see what I mean, take this example:

let a: { [prop: number]: number };  // all numeric props with number values
const b = { 1: 1, 2: 2, "foo": "bar" };
a = b;  // now `a` has a "foo" property with a string value, even though its index signature claims it's all numbers
for (const i in a) {  // `i` can only safely be typed as string
    const x = a[i];  // oops, what is the type of `x` here?
    console.log(x);  // should print 1 -> 2 -> "bar"
}

@rosenbergd
Copy link
Author

rosenbergd commented Jun 24, 2021

What you're describing here is not related to my point, but it is its own issue, it shouldn't allow a = b; since b is really a { [prop: number]: string | number, [prop: string]: string | number } which is not type compatible with a (typescript doesn't seem to pick this type up automatically, but try to add the type and you'll see it refuses to work), but that's something you should probably open another issue for since it has nothing to do with what I'm saying.

I didn't say that the for loop's typeof i should be a number (since the underlying behaviour of using a string there is really coming from JS and not TS, try both an array and object with "number" indexes in JS and you'll see they're both really string indexes), only that the type of a's indexes should be possible to specify as a type that explains the real behaviour more accurately, i.e. a number in string format.

I'm aware that it's possible for such a scenario as you've specified to happen, but that doesn't take away the need to be more concise when specifying the type, otherwise why would I even specify a type in the first place?

@RyanCavanaugh
Copy link
Member

it shouldn't allow a = b;

This is a common misunderstanding. A numeric index signature is not a restriction on what kind of keys an object may have. It's a restriction on what type of values can exist at numerically-named property slots.

@fatcerberus
Copy link

fatcerberus commented Jun 24, 2021

That was my point: even if there were a numstring type, it wouldn't be safe to have i be that type, since the object might have more properties at runtime than its index signature/property list suggests. i can only ever be string, and then you're right back to square one (if you index a with an opaque string key, TS doesn't know at compile time what type of value you'll get back due to structural subtyping).

@rosenbergd
Copy link
Author

That's not my point though, I'm not asking to change the type of i, I'm asking for a type that can indicate my intent better to indicate that i will be a string but the indexes should be based off a numerical value (and yes, I'm aware that someone could break that expectation, but then that's on them), string is the most accurate one but it doesn't portray my intent that the original index should be numerical even though it's a string with number characters.

I'm not even asking for i to be of type numstring, I very specifically showed in my example that i is a string, I just don't want to specify number in the type of the key when initially creating a since it is misleading as it's not a number when checked, but still need a way to indicate that the key should be numerical or even just a number (i.e. a[1] = 123; and a['1'] = 123; should both be allowed since they really are the same thing).

This is about the intent being shown, not an indication that it's the only possible type since all types can be cheated, especially during runtime.

@RyanCavanaugh
Copy link
Member

If literally all you want is to have the ability to write

 { [n: numstring]: T }

instead of

 { [n: number]: T }

but otherwise have identical semantics, I don't think this is an in-scope suggestion.

Otherwise explaining what deficits you see with ${number} would be useful

@rosenbergd
Copy link
Author

rosenbergd commented Jun 24, 2021

Using ${number} causes the object to reject all properties

const a: {[prop: `${number}`]: number} = {};

// none of these are allowed
a[`${1}`] = 123;
a[`1`] = 123;
a[1] = 123;
a['2'] = 321;

numstring is just a suggestion, but type alias is not allowed even if it's an alias to an allowed type:

type numstring = number;
const a: {[prop: numstring]: number} = {}; // this refuses as well stating: An index signature parameter type cannot be a type alias. Consider writing '[prop: number]: number' instead.

Neither potential way for specifying something at least resembling what I want to portray in the code works.

That's besides the benefit that being able to use any of these formats gives:

a[1] = 123;
a['2'] = 321;

I'm just not specifying this benefit anymore since I'm aware of work being done to allow that right now on PR #44512

@cvennevik
Copy link

I ran into the issue of lacking a numeric string type just now. Here is my own motivating example.

I am working with an API returning JSON data with numeric string values.

URL to example data:
https://api.met.no/weatherapi/sunrise/2.0/.json?lat=40.7127&lon=-74.0059&date=2023-02-02&offset=-05:00

Relevant snippet of the data:

{
    "desc":"LOCAL MOON STATE * MOON PHASE= 39.3 (waxing gibbous)",
    "time":"2023-02-02T00:00:00-05:00",
    "value":"39.279116267"
}

Our logic checks whether the "value" property is less than or equal to some numbers.

if (moonphase.value <= 25) {
    // some logic
} else if (moonphase.value <= 50) {
    // some other logic
} // and so on

JavaScript is able to compare the numeric strings and numbers correctly. '30' <= 25 is false, '30' <= 50 is true. This works well for our use case.

When I convert the code to TypeScript, I don't have a clean way of typing this property. If I declare the type to be ${number}, it tells me "Operator '<=' cannot be applied to types 'string' and 'number'".

I do not want to explicitly convert the numeric string to a number if I do not have to, as it makes the code a little more complicated. My current workaround is to declare the type to be number, but this allows some incorrect usages if we forget it is secretly a string. I would much prefer a type that supports this use case cleanly.

@mkantor
Copy link
Contributor

mkantor commented Dec 6, 2024

@rosenbergd:

Using ${number} causes the object to reject all properties

const a: {[prop: `${number}`]: number} = {};

// none of these are allowed
a[`${1}`] = 123;
a[`1`] = 123;
a[1] = 123;
a['2'] = 321;

These days all except the a[1] assignment are allowed. Does that mean `${number}` is now good enough? If so this issue could be closed.

@rosenbergd
Copy link
Author

This was an old ticket, so I don't remember the specific use-case I needed it for, but if it's now allowing those, then it probably does solve the original issue.

I most likely worked around this limitation in another way. I'll try looking at my git history for the project I was most likely working on at the time to see if I find anything that looks familiar and will see if I can figure out what I wanted to do then.

@rosenbergd
Copy link
Author

Looks like it's still not quite working as expected. The indexes are fine, but once it's used in a loop, the type of i is not set correctly, so something like this will not be allowed:

for (const i in a) {
    console.log(a, a[i], typeof i); // first iteration: { "1": 123, "2": 321 }, 123, "string"
    // above `a[i]` is marked as an error
}

Because i will be treated as a basic string and conflict with the ${number} type expected.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants