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

Feature request: Ability to parse a stringified number on type-level #47141

Closed
5 tasks done
devanshj opened this issue Dec 13, 2021 · 13 comments Β· Fixed by #48094
Closed
5 tasks done

Feature request: Ability to parse a stringified number on type-level #47141

devanshj opened this issue Dec 13, 2021 · 13 comments Β· Fixed by #48094
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@devanshj
Copy link

devanshj commented Dec 13, 2021

Suggestion

πŸ” Search Terms

parse a number from stringified number on type-level, infer a number literal from a template string literal, template string literal to number literal

βœ… 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

Currently there is no way to parse "100" into 100 on type-level. One could try something like...

type T0 = "100" extends `${infer X}` ? X : never
// `T0` is `"100"`

But T0 is "100", rightfully so. And inferring 100 instead would be a breaking change. Let's try this...

type StringifyNumber<T extends number> = `${T}`

type T1 = "100" extends StringifyNumber<infer X> ? X : never
// `T1` is `number`

type T2 = "foo" extends StringifyNumber<infer X> ? X : never
// `T2` is `number`

Here T1 is number when it could have been inferred as 100. The feature request is to make T1 100, this too would be a breaking change but I think it'd break things a lot less. I think it makes sense to infer the narrowest possible type when we can.

πŸ“ƒ Motivating Example

Type-level arithmetic used to be impossible with large number prior to template literal types. But now we can stringify numbers on type-level then split them into digits and then do a carry-save addition. This makes type-level arithmetic very fast and "doable". Example adding 12345 & 6789...
image

But then there's no way to parse the string back to a number.

Type-level arithmetic makes working with dependent-ish types more complete. It'd be great we can have this feature, thanks!

@jcalz
Copy link
Contributor

jcalz commented Dec 14, 2021

Looks like a duplicate of #26382.

I would love to have built-in support for StringToNumber<T> and arithmetic operations like Add<M, N>, Subtract<M, N>. I'd think the primary motivation for StringToNumber<T> would be to represent in the type level the conversion of strings to numbers at the value level (e.g., <T extends string>(x: T): StringToNumber<T> => +x or at least <T extends string>(x: T) => +x as StringToNumber<T>).

It's an... interesting approach to say that the motivating example for StringToNumber<T> is that it enables us to implement Add and Subtract and such via template literal types. That's really neat, but I suspect that splitting strings and shuffling decimal digits around might not be the fastest nor most robust way for the TypeScript compiler to do arithmetic. It's kind of like a child asking their parents for a smartphone "because it's the final ingredient of my elaborate Rube Goldberg machine". Come on, kid, say "so I can text you in case of emergencies" or make up something about how it'll help you study for school. You're saying the quiet part out loud! 😜

@devanshj
Copy link
Author

devanshj commented Dec 14, 2021

Hahaha yeah I was too honest with the motivation part, I just innocently wrote what motivated me instead of writing a more saner motivation that would perhaps motivate the typescript team :P and very apt anology btw xD

But yeah as you mentioned perhaps another motivation is to make the Number-like function's type more complete. Example as of today this wouldn't compile even when there are no problems with it...

declare let x: [string, number]
let foo: string = x[Number("0")]
//  ~~~
// Error: `string | number` is not assignable to `string`

With toNumber("0") which utilizes this feature the above code would compile.

Although I would not call this a duplicate of #26382 because it's not, but not even a spiritual duplicate because this feature would only enable arithmetic of number literals, whereas the linked feature (if implemented fully instead of just literals) would have to handle generics too. Which would basically make typescript a dependently typed language, you'll be able to prove things with it...

declare const lhs =
  <A extends number, B extends number>
    (a: A, b: B) => (A + B) * (A + B)

declare const rhs =
  <A extends number, B extends number>
    (a: A, b: B) => A * A + A * B + B * A + B * B

const prove =
  <A extends number, B extends number>
    (a: A, b: B) => {

  let x = lhs(a, b)
  let y: typeof x = rhs(a, b)
  // If this compiles, which it would, lhs and rhs are equal
}

So having things like + and * on type-level are HUGEEEE features. And if we're not going to fully implement it and decide that that A + B would simply become number then meh, still useful with literals, but loses it's charm for me.
Although ofc it's still better than doing arithmetic with tricks and when involving decimals you'd have to make it IEEE 754 compat and what not, so yeah even arithmetic that only works with literals is great.

Regarding a StringToNumber intrinsic type: I don't mind it but I think I'd prefer keeping it as it is the description, feels more simple to me. It does make the feature less visible but then a lot of typescript features and tricks are like that :P

@jcalz
Copy link
Contributor

jcalz commented Dec 14, 2021

I didn't mean to bikeshed the naming; I was using StringToNumber<T> as a stand-in for however it happens to implemented. If it's not intrinsic, that's fine, as long as you could define type StringtoNumber<T extends string> = ... yourself.

@devanshj
Copy link
Author

Ah gotcha, even I didn't mean to bikeshed on the name, I thought you were suggesting that instead of resolving "100" extends StringifyNumber<infer X> ? X : never to 100 let's keep as it is ie number and instead have a type StringToNumber<T> = intrinsic which would be used instead of the inference trick.

@RyanCavanaugh RyanCavanaugh added Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript labels Dec 14, 2021
@RyanCavanaugh
Copy link
Member

You can write

interface StringToNumber {
    "0": 0,
    "1": 1,
    "2": 2,
    "3": 3, // etc
}

if the numbers involved are too high for this approach to be reasonable, you are doing math that is not appropriate to be done in the type system.

@devanshj
Copy link
Author

The math part was just an example, what if one wants to make this compile...

const toNumber = <N extends number>(s: StringifiedNumber<N>) => Number(s) as N
type StringifiedNumber<N extends number> = `${N}`

const x: 1000 = toNumber("1000")

A type system that allows writing parsers, that added tail call optimization so that users can operate on larger strings and many more things, doesn't want to allow a simple operation of going from "1000" to 1000 doesn't make sense to me sir :) I'd love if you reconsider it.

You can always state that "We're adding this feature but it's not meant from doing typelevel maths with large numbers" and then people who still would do it (like in case of the rest of typescript features :P) would be on their own.

@RyanCavanaugh
Copy link
Member

We're adding this feature but it's not meant from doing typelevel maths with large numbers

For context, this never works.

@devanshj
Copy link
Author

devanshj commented Dec 14, 2021

For context, this never works.

Haha yeah I know hence the "like in case of the rest of typescript features :P"

But what I meant was by stating that, the risk of "misusing" it is transferred to them. I'd do the same if I were to publish a library that does typelevel maths, I'd say "TypeScript team has said not to do this with large numbers" and then the risk further gets transferred to the person who actually would be responsible.

In this way one is not deciding for someone else how much risk they should take, as different people have different amounts of courage to take risks. But at the same time I understand not everyone wants to operate from this model, and that's okay.

Eitherway, I'm happy with typescript :P

@crazyones110
Copy link

We can have this trick to parse string to number from 0 to 999 since TS 4.5 has tail recursion optimization

type ToNumber<T extends string, R extends any[] = []> =
    T extends `${R['length']}` ? R['length'] : ToNumber<T, [1, ...R]>;

@devanshj
Copy link
Author

Ah that's nice especially the fact it can go upto 999

@crazyones110
Copy link

Ah that's nice especially the fact it can go upto 999

Yeah, this PR may help

@sno2
Copy link
Contributor

sno2 commented Jan 16, 2022

I think this a duplicate of Should #42938 probably supersede this? In that case, you could just make it part of the syntax.

@devanshj
Copy link
Author

devanshj commented Jan 16, 2022

Yeah it's essentially a duplicate as both issues are asking for the same feature

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants