Skip to content

Type Math.min & Math.max using generic #30924

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

Open
4 of 5 tasks
peat-psuwit opened this issue Apr 14, 2019 · 9 comments
Open
4 of 5 tasks

Type Math.min & Math.max using generic #30924

peat-psuwit opened this issue Apr 14, 2019 · 9 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@peat-psuwit
Copy link

peat-psuwit commented Apr 14, 2019

Search Terms

  • Math.min generic
  • Math.max generic

Suggestion

Currently, Math.min is typed as:

min(...values: number[]): number;

However, I would like to change the definition to:

min<T extends number>(...values: [T, ...T[]]): T;
min(): number; // Because this will return Infinity

Because Math.min should never return things that aren't its input. (Except when non-number is passed in, then it'll return NaN. But that's impossible with this typing.)

(Okay, there's another case: if no value is passed in, it'll return Infinity. Unfortunately, there's no literal type for Infinity so we can't type that correctly. And AFAIK there's no way to force at least 1 parameter with rest args. Updated: there is, using tuple type: [T, ...T[]]. Proposal updated. Still, it would be nice to have literal Infinity type.)

(The same applies with Math.max, except Infinity is now -Infinity)

Use Cases

Let's say I have this type:

type TBankNoteValues = 1 | 5 | 10 | 20 | 50 | 100;

And I want to know what is the highest-value banknote I have in the array. I could use Math.max to find that out. But with the current typing, the return value isn't guaranteed to be TBankNoteValues.

let values: TBankNoteValues[] = [50, 100, 20];
let maxValues = Math.max(...values); // number

And now I can't pass maxValues to a function that expects TBankNoteValues anymore.

Examples

type TBankNoteValues = 1 | 5 | 10 | 20 | 50 | 100;

let values: TBankNoteValues[] = [50, 100, 20];
let maxValues = Math.max(...values);
// Current type: number
// Expected type: TBankNoteValues

Playground link

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
    • Shouldn't be any. Anything that expects the old signature, TypeScript should just infer T to number.
    • Now that I think about it, this make the return type of the function narrows down. So, it could make type inference on a variable declaration changes unexpectedly. An explicit type annotation should fix this.
  • 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, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@krryan
Copy link

krryan commented Apr 15, 2019

We created our own minOf and maxOf signatures to do this, would be great if it was just part of the library.

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Apr 15, 2019
@qn0361
Copy link

qn0361 commented Jun 10, 2021

We created our own minOf and maxOf signatures to do this, would be great if it was just part of the library.

it would be very nice if you shared these types you created

@BernardoMariano
Copy link

BernardoMariano commented Oct 1, 2022

I don't think annotating the generic as <T extends number> is correct because T don't necessarily have to extend number, it is stated on the language that:

Returns NaN if any of the parameters is or is converted into NaN.

Meaning that it could be a string as well.

Math.min("5", "6e10") // correctly returns 5
Math.max("5", "6e10") // correctly returns 60000000000

@MartinJohns
Copy link
Contributor

@BernardoMariano With that argument you can scrap TypeScript completely, because basically all of JavaScript has well-defined results for operations, e.g. for {} + []. See also: What does "all legal JavaScript is legal TypeScript" mean?

@burtek
Copy link

burtek commented Oct 1, 2022

I agree with @MartinJohns as TS is supposed to make app as type-safe as possible and as type-sane as possible. Math.min and Math.max, while allowing other data types, are meant to compare numbers and as such, should be typed as allowing only number-like arguments.

That being said, I don't agree with @peat-psuwit 's motion to change Math.min and Math.max types. First of all a Array<1 | 2 | 3> type won't play nice with min<T extends number>(...values: [T, ...T[]]): T; signature (as the former can have 0 or more items, while function expects 1 or more) and thus TS would fallback to min(): number; signature (and infer return type as number). Secondly, this is really a app-specific (case-specific) typing that doesn't necessarily need to exist in all apps and that - if needed - can be achieved by augmenting global module, creating a custom module with correct typing, or just casting the return value. If we wanted to introduce such a change here, we would soon have to re-type half of the library following similar request for other library parts.

@krryan
Copy link

krryan commented Oct 1, 2022

First of all a Array<1 | 2 | 3> type won't play nice with min<T extends number>(...values: [T, ...T[]]): T; signature (as the former can have 0 or more items, while function expects 1 or more) and thus TS would fallback to min(): number; signature (and infer return type as number).

An overload trivially resolves that. Of course this shouldn’t be the only typing.

Secondly, this is really a app-specific (case-specific) typing that doesn't necessarily need to exist in all apps and that - if needed - can be achieved by augmenting global module, creating a custom module with correct typing, or just casting the return value.

It is always accurate. Why shouldn’t it be as accurate as possible?

If we wanted to introduce such a change here, we would soon have to re-type half of the library following similar request for other library parts.

Yes. It might not be a top priority, but again, why shouldn’t things be accurate?

@peat-psuwit
Copy link
Author

First of all a Array<1 | 2 | 3> type won't play nice with min(...values: [T, ...T[]]): T; signature (as the former can have 0 or more items, while function expects 1 or more) and thus TS would fallback to min(): number; signature (and infer return type as number).

Hmm... interesting. My proposal addresses 1 or more argument and exactly 0, but forgot 0 or more case. I think

min<T extends number>(...values: [T, ...T[]]): T;
min(...values: number[]): number; // Because 0 args will return Infinity

should handle it. Because if you expect 0 arguments as a possibility, a return value of Infinity is also a possibility. And since it's impossible to type literal Infinity (I tried), number is the only type which includes both T and Infinity [1].

[1] Is there a proposal somewhere which allow typing literal Infinity?

@MartinJohns
Copy link
Contributor

And since it's impossible to type literal Infinity (I tried)

Related: #32277

@LorenzoBloedow
Copy link

Any updates on this? I'm currently having to use type assertion (as), would be nice to have better type-safety.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

8 participants