TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Add math intrinsic types

Open skeate opened this issue 3 years ago • 20 comments

Fixes #26382

With intrinsic types, it seemed like some simple type-level math would be a straightforward addition, so I wanted to try it out.

This adds several new intrinsic types to perform numerical operations on number literal types:

  • Floor<N> calls Math.floor on N
  • Ceil<N> calls Math.ceil on N
  • Round<N> calls Math.round on N
  • Add<M, N> evaluates to M+N
  • Multiply<M, N> evaluates to M*N
  • Subtract<M, N> evaluates to M-N
  • Divide<M, N> evaluates to M/N (floating point division)
  • ~~LessThan<M, N> is M if M < N, otherwise never~~ (removed, see comment below)
  • ~~GreaterThan<M, N> is M if M > N, otherwise never~~ (removed, see comment below)

The goal is to permit things like type-safe sized containers, beyond simple arrays/tuple-types, or anything else for which you might want to do some simple type-level math without having to resort to various hacky solutions (not that these are bad solutions, but they usually are hard to understand, are full of limitations, and make the typechecker do more work than should be necessary for this kind of thing).

skeate avatar Mar 10 '22 04:03 skeate

The TypeScript team hasn't accepted the linked issue #26382. If you can get it accepted, this PR will have a better chance of being reviewed.

typescript-bot avatar Mar 10 '22 04:03 typescript-bot

CLA assistant check
All CLA requirements met.

ghost avatar Mar 10 '22 04:03 ghost

may i understand the specific use case you wanna address? imho this might not be a case that TypeScript is designed to solve.

Cryrivers avatar Mar 14 '22 08:03 Cryrivers

LessThan<M, N> is M if M < N, otherwise never GreaterThan<M, N> is M if M > N, otherwise never

Shouldn't LessThan return a boolean literal type instead of number/never?

Jack-Works avatar Mar 19 '22 03:03 Jack-Works

LessThan<M, N> is M if M < N, otherwise never GreaterThan<M, N> is M if M > N, otherwise never

Shouldn't LessThan return a boolean literal type instead of number/never?

Two reasons it's a number:

  1. For use as an argument type eg. const at: <K, N>(index: LessThan<K, N>) => <A>(collection: SizedCollection<N, A>) => A
  2. All the Calculation types return numbers this way, so e.g. this is true: https://github.com/microsoft/TypeScript/pull/48198/commits/eaa6142f8c64039a3c38427af7c2b6e42a8fa7fa#diff-e9fd483341eea176a38fbd370590e1dc65ce2d9bf70bfd317c5407f04dba9560R5182

That said, those two are the ones I am least sure about. I think something like what's suggested in #43505 would be far better.

skeate avatar Mar 19 '22 04:03 skeate

What about Subtract and Divide?

roobscoob avatar Mar 22 '22 17:03 roobscoob

https://github.com/unional/type-plus/tree/main/ts/math My hacky example. :)

unional avatar Mar 22 '22 22:03 unional

Updated this.

  • Removed the two comparisons.
    1. They were a bit confusing; they fit a use case I had in mind but might not be general enough.
    2. As noted in another comment, true inequality types like in #43505 would be better, and I would worry adding this kind of half-measure would add resistance to that better solution.
    3. Most importantly, they can be implemented using the others added in here. See below.
  • I added Subtract and Divide. I originally didn't have them because, I thought, type Subtract<M, N> = Add<M, Multiply<N, -1>> but you can't do the same thing for Divide. Divide<x, 0> evaluates to never

Implementing the previous LessThan and GreaterThan with the others:

type LessThan<M, N> = `${Subtract<M, N>}` extends `-${number}` ? M : never
type GreaterThan<M, N> = `${Subtract<N, M>}` extends `-${number}` ? M : never

skeate avatar May 04 '22 23:05 skeate

This could be useful for example for the quickjs-API. quickjs borrows error handling from the C API, and therefore values < 0 represent an error, whereas 0 (or >= 0 on some functions) represents success. With this PR it is possible to enforce error handling through the type system.

Gottox avatar Nov 06 '22 10:11 Gottox

@skeate Do you have any plans to merge it with latest changes? Otherwise this PR is amazing and I'm so happy I found it!

kungfooman avatar May 30 '24 12:05 kungfooman

I reintegrated this into the latest TS version (HEAD) and it still works like a charm, great work!

Playing around with this is interesting and I came across this:

type Add10<T extends number> = Add<T, 10>;
type Add20<T extends number> = Add<10, Add<10, T>>;
type tmpA = Add10<20>; // Outputs: 30
type tmpB = Add20<20>; // Outputs: 40

If you hover over Add20:

image

Should something like Add<10, Add<10, T>> be simplified into Add<20, T>? I assume this would explode the scope, but interesting anyway to contemplate the pros and cons. How do other languages handle this case?

kungfooman avatar May 30 '24 14:05 kungfooman

I reintegrated this into the latest TS version (HEAD) and it still works like a charm, great work!

😮 I'm surprised; the conflicts looked pretty severe so I thought it'd basically require reimplementing the whole PR. If you want, I think you could PR that to my fork and then it should show up here. Though I'm not very hopeful that this will actually get merged; it was more a proof of concept/learning exercise.

I don't think that e.g. simplifying Add<10, Add<10, T>> would be in scope for this, as I suspect it'd require a lot of special handling of these new intrinsics.

I'm not sure if the more floating-point-oriented types have much utility. Because of the inherent imprecision of floating point math, it doesn't seem like it belongs in the type system. I was hesitant to even include Divide, thinking maybe integer division makes more sense. I went with it because you could get integer division with that and Integer<T>, but in retrospect I think integer division + modulus would be more useful.

If you have a use case for them, though, I'd be interested to hear it

skeate avatar May 30 '24 15:05 skeate

@kungfooman thank you for your PR to bring it up to date. I hope you don't mind but I took the diff from microsoft/TypeScript:main and remade it as a single commit, just to avoid a gnarly history. I added you as a co-author in the commit message though.

skeate avatar Jun 01 '24 19:06 skeate

.. Didn't mean to un-draft this PR. Oops.

skeate avatar Jun 01 '24 20:06 skeate

Hello @skeate, thank you for your work which I find really great.

Do you think you'll have time to finish your pull request soon? What can we do to help you? I'm really looking forward to using your new feature :)

toofff avatar Jan 14 '25 19:01 toofff

@toofff You can fix the unit tests and make a PR towards https://github.com/skeate/TypeScript/tree/main

kungfooman avatar Jan 15 '25 10:01 kungfooman

@toofff You can fix the unit tests and make a PR towards https://github.com/skeate/TypeScript/tree/main

I'd like to give it a try, plus I've never worked in your repo before. I'll keep you posted.

thank you for your comment @kungfooman

toofff avatar Jan 15 '25 21:01 toofff

I just opened https://github.com/skeate/TypeScript/pull/2, which fixes a few things. All the tests now pass on my fork.

james-pre avatar Mar 12 '25 04:03 james-pre

Looks like you're introducing a change to the public API surface area. If this includes breaking changes, please document them on our wiki's API Breaking Changes page.

Also, please make sure @DanielRosenwasser and @RyanCavanaugh are aware of the changes, just as a heads up.

typescript-bot avatar Mar 12 '25 12:03 typescript-bot

Quick note for the TS team- there are no breaking changes.

james-pre avatar Mar 12 '25 14:03 james-pre

Could a maintainer please take a look at this? It's been over 4 months.

james-pre avatar Jul 18 '25 02:07 james-pre

@james-pre Not sure what the point of looking at this PR would be, as the related issue #26382 hasn't been accepted. The maintainers surely have better things to do than look at PRs implementing unaccepted features (that they'd have to maintain if accepted).

Also relevant: https://github.com/microsoft/typescript/wiki/faq#time-marches-on

MartinJohns avatar Jul 19 '25 13:07 MartinJohns

We indeed cannot review PRs for features that aren't accepted; the language design must come before implementation.

RyanCavanaugh avatar Jul 29 '25 18:07 RyanCavanaugh

Thank you for the feedback @RyanCavanaugh :rocket:

I'm testing this and trying to find some rough edges. One is in Multiply, when multiplying something with 0 it could/should always simplify to 0. We could either fix the exposed TS type or probably better to implement that in checker.ts.

kungfooman avatar Aug 11 '25 11:08 kungfooman

So I implemented the multiply-by-zero-is-zero here: https://github.com/skeate/TypeScript/pull/3

But we need more thoughts for the language design as pointed out to get a usable/nice proposal going.

Since we are dealing with math, IMO we have to follow its rules/identities.

What I can think of so far besides what we already have:

  • Adding 0 to T is just T: Add<T, 0> or Add<0, T> :arrow_right: T
  • Subtracting 0 from T is also just T: Subtract<T, 0> :arrow_right: T

Some more complicated cases:

  • Add<T, -T> or Add<-T, T> :arrow_right: 0
  • Add<T, Multiply<T, -1>> :arrow_right: 0

I don't want to turn this into Mathematica, but a certain degree of mathematical insight should be present to simplify expressions and keeping performance up... any more ideas?

kungfooman avatar Aug 11 '25 16:08 kungfooman