Typescript: Date.getMonth could return a union of all possible values

Created on 24 Jan 2020  路  8Comments  路  Source: microsoft/TypeScript

Search Terms

Suggestion

According to the MDN docs, Date.getMonth() always returns a number between 0 and 11 (inclusive):

An integer number, between 0 and 11, representing the month in the given date according to local time. 0 corresponds to January, 1 to February, and so on.

Currently, the lib.d.ts definition states that this function returns number.

So I suggest du change the definition of getMonth() from:
https://github.com/microsoft/TypeScript/blob/91ffa1c752ac26882b1426fb4c9012701c1d908e/lib/lib.es5.d.ts#L750-L751

...to something like:

interface Date {
    // ...
    /** Gets the month, using local time. */
    getMonth(): 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11; // (or using a type definition for this union)
    // ...
}

Use Cases

This would be beneficial if user code contains something like if (date.getMonth() == 12). The compiler can then infer that the entire if branch is dead code.
Also, it serves as a quick documentation to let the user know that the return value is in fact a 0-based (not 1-based) month. The number of months in a year is also unlikely to change as well as the base (0/1) of the month index that this function returns.

Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code <-- I'm not sure about that. It should not be breaking, since 0 | ... | 11 is a subset of number.
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.

Remarks

This could likewise be done for getUTCMonth().

There are other methods like getMinutes() and getSeconds(). As they return something in the range of 0 to 60, could also be modeled using a union. However, I think that a large union like this may be considered differently.

Also, there are setters for these values (like setMonth()). Changing the parameter of this function to take a 0 | ... | 11 would actually be a breaking change. It would not reflect the runtime behaviour. setMonth(15) would be a valid statement that will increment the year by 1 and the months by 3. So the setters should be kept as using number.

If this is up for grabs, I'd take this.

Awaiting More Feedback Suggestion

Most helpful comment

I can imagine this being a breaking change for code that does anything like this:

let month = new Date().getMonth();
// next month
month = (month + 1) % 12; // error!
//~~~ <-- number not assignable to 0 | ... | 11

but I don't know how prevalent that would be in practice. Could be related to #15645 and/or #26382.

All 8 comments

I can imagine this being a breaking change for code that does anything like this:

let month = new Date().getMonth();
// next month
month = (month + 1) % 12; // error!
//~~~ <-- number not assignable to 0 | ... | 11

but I don't know how prevalent that would be in practice. Could be related to #15645 and/or #26382.

We don't have NaN type literals yet.
new Date(NaN).getMonth() is NaN.

NaN type literals

I guess #28682 is currently the canonical issue for that.

We don't have NaN type literals yet.

Well, then we are not able to map the types properly to the runtime behavior.

Even if we had the NaN type, returning NaN | 1 | ... | 11 would even be misleading in the most places. Most NaN checks would likely be unnecessary.

So at the moment, this may not be possible without sacrificing correctness or (if NaN would be available) usability.

Related issue is numeric ranges: #29119

I don't think we'd take this without further feedback since it's possibly a breaking change and might interact badly with conditional types. If numeric range types ever happen, that'd be a much better fit.

Well, we have ReadonlyArray<> and Array<>.

I don't see why we can't have "regular" Date (where Date.getTime() is possibly NaN) and something like DefinedDate (where DefinedDate.getTime() is not NaN).

DefinedDate.getMonth() could be 0|1|...|10|11.
While Date.getMonth() could be NaN|0|1|...|10|11.

That would require a type guard/assertion to go from Date to DefinedDate, though.
!isNaN(myDate.getTime())

If we had literally everything we could have, the language would be impossibly complex. So we need to have the things we need, not just the things that are possible.

Well, maybe not part of TS officially.

But someone could just drop the following into their project,

interface DefinedDate extends Date {
  getMonth () : 0|1|/*snip*/|10|11;
}
function assertDefinedDate (d : Date) : asserts d is DefinedDate {
  if (isNaN(d.getTime())) {
    throw new TypeError(`getTime() is NaN`);
  }
}

declare const d : Date;
assertDefinedDate(d);
//0|1|...|10|11
const month : d.getMonth();
Was this page helpful?
0 / 5 - 0 ratings

Related issues

bgrieder picture bgrieder  路  3Comments

weswigham picture weswigham  路  3Comments

uber5001 picture uber5001  路  3Comments

blendsdk picture blendsdk  路  3Comments

Antony-Jones picture Antony-Jones  路  3Comments