Add math intrinsic types
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>callsMath.flooronNCeil<N>callsMath.ceilonNRound<N>callsMath.roundonNAdd<M, N>evaluates to M+NMultiply<M, N>evaluates to M*NSubtract<M, N>evaluates to M-NDivide<M, N>evaluates to M/N (floating point division)- ~~
LessThan<M, N>isMif M < N, otherwisenever~~ (removed, see comment below) - ~~
GreaterThan<M, N>isMif M > N, otherwisenever~~ (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).
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.
may i understand the specific use case you wanna address? imho this might not be a case that TypeScript is designed to solve.
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?
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:
- For use as an argument type eg.
const at: <K, N>(index: LessThan<K, N>) => <A>(collection: SizedCollection<N, A>) => A - All the
Calculationtypes returnnumbers 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.
What about Subtract and Divide?
https://github.com/unional/type-plus/tree/main/ts/math My hacky example. :)
Updated this.
- Removed the two comparisons.
- They were a bit confusing; they fit a use case I had in mind but might not be general enough.
- 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.
- Most importantly, they can be implemented using the others added in here. See below.
- I added
SubtractandDivide. 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 forDivide.Divide<x, 0>evaluates tonever
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
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.
@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!
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:
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?
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
@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.
.. Didn't mean to un-draft this PR. Oops.
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 You can fix the unit tests and make a PR towards https://github.com/skeate/TypeScript/tree/main
@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
I just opened https://github.com/skeate/TypeScript/pull/2, which fixes a few things. All the tests now pass on my fork.
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.
Quick note for the TS team- there are no breaking changes.
Could a maintainer please take a look at this? It's been over 4 months.
@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
We indeed cannot review PRs for features that aren't accepted; the language design must come before implementation.
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.
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>orAdd<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>orAdd<-T, T>:arrow_right: 0Add<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?