microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.6k stars 12.44k forks source link

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

Open peat-psuwit opened 5 years ago

peat-psuwit commented 5 years ago

Search Terms

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:

krryan commented 5 years ago

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

qn0361 commented 3 years ago

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 commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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

Related: #32277

LorenzoBloedow commented 1 year ago

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