-
Notifications
You must be signed in to change notification settings - Fork 31
Add types to Slots. #71
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool! I built something similar as an experiment, but I like yours better. Simpler.
I suggested a few changes, otherwise good to merge once those are fixed.
For the runtime parts of this PR (new variables, etc.), is it worth porting those changes back to proposal-temporal to reduce the difference between the codebases to reduce the complexity of future porting in the other direction?
[CALENDAR]: Temporal.Calendar; | ||
|
||
// Date, YearMonth, MonthDay common slots | ||
[DATE_BRAND]: unknown; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should be boolean or true
. Not sure which would be best, but probably true
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC the date/YM/MD brand check is currently implemented using only HasSlot — it doesn't matter what's in the slot, only that it is present.
[ISO_MILLISECOND]: number; | ||
[ISO_MICROSECOND]: number; | ||
[ISO_NANOSECOND]: number; | ||
[CALENDAR]: Temporal.Calendar; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually think this is CalendarProtocol, because non-built-in calendars can also be stored.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't work, as it triggers ~50 errors about assignability:
Argument of type 'CalendarProtocol' is not assignable to parameter of type 'Calendar'.ts(2345)
I've started to see this show up elsewhere when I turn on more of the compiler options. It makes sense - Calendar
is intentionally different from CalendarProtocol
from the perspective of users, and it seems everywhere we accept a CalendarProtocol
we do call ToTemporalCalendar
which should convert a CalendarProtocol
to a Calendar
. At least in my branch, that method isn't typed and in most of the places we do this conversion it looks a bit something like
constructor(..., calendar: Temporal.CalendarProtocol | string = ES.GetISO8601Calendar()) {
...
calendar = ES.ToTemporalCalendar(calendar);
// Note: if the arguments are not passed,
// ToIntegerThrowOnInfinity(undefined) will have returned 0, which will
// be rejected by RejectISODate in CreateTemporalDateSlots. This check
// exists only to improve the error message.
if (arguments.length < 3) {
throw new RangeError('missing argument: isoYear, isoMonth and isoDay are required');
}
ES.CreateTemporalDateSlots(this, isoYear, isoMonth, isoDay, calendar);
}
which doesn't compile as CreateTemporalDateSlots
shouldn't accept CalendarProtocol
, only Calendar
. If this is rewritten to no longer reassign the parameter, and ToTemporalCalendar is typed correctly, then I think I can say with certainty that all user calendars (and likely, all user TimeZones) are converted from the Protocol
version to the internal version.
I was wondering if whether some sort of similar difference in types would be useful to enforce that these coercion methods are actually called and the results used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this is usually the param reassignment problem. Once we fix that, then I think most of these errors should go away-- any others can probably be addressed with type assertions or (worst case) additional variable renaming.
Too bad that TS doesn't narrow types from reassignment. Here's me making the case for why it should: microsoft/TypeScript#45870 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ToTemporalCalendar can't narrow the type, because of plain object calendars. At first glance I think it is actually correct that CreateTemporalDateSlots should accept CalendarProtocol, for that reason.
@@ -39,8 +42,52 @@ export const NANOSECONDS = 'slot-nanoseconds'; | |||
// Calendar | |||
export const CALENDAR_ID = 'slot-calendar-identifier'; | |||
|
|||
interface Slots { | |||
// Instant | |||
[EPOCHNANOSECONDS]: bigInt.BigInteger; // number? JSBI? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely not number
. Seems like bigInt.BigInteger
is the right type, and maybe change that once JSBI is in.
|
||
// ZonedDateTime | ||
[INSTANT]: Temporal.Instant; | ||
[TIME_ZONE]: Temporal.TimeZone; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is TimeZoneProtocol, for same reason as CalendarProtocol above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried this PR in my branch and here's a few more suggestions.
@@ -1,3 +1,6 @@ | |||
import { Temporal } from '.'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect that consistently only importing types (not implementations) works better. Not 100% sure, but I think I've seen cases where mixing the two causes weird loops in type resolution that the TS compiler complained about.
import { Temporal } from '.'; | |
import type { Temporal } from '..'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been running into this issue all the time lately, where vscode suggests one or the other import and the two aren't assignable to each other. It would be great to try and 'exclude' temporal.ts (the actual module file) from being an import suggestion so that only the typings would show up instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, if you find out how to do that let me know! Very annoying.
@@ -51,9 +98,9 @@ export function HasSlot(container, ...ids) { | |||
const myslots = GetSlots(container); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably type HasSlot while we're at it:
export function HasSlot(container: unknown, ...ids: (keyof Slots)[]) {
const slots = new WeakMap(); | ||
export function CreateSlots(container) { | ||
export function CreateSlots(container): void { | ||
slots.set(container, Object.create(null)); | ||
} | ||
function GetSlots(container) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
function GetSlots(container) { | |
function GetSlots(container: AnyTemporalType) { |
@@ -51,9 +98,9 @@ export function HasSlot(container, ...ids) { | |||
const myslots = GetSlots(container); | |||
return !!myslots && ids.reduce((all, id) => all && id in myslots, true); | |||
} | |||
export function GetSlot(container, id) { | |||
export function GetSlot<KeyT extends keyof Slots>(container, id: KeyT): Slots[KeyT] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export function GetSlot<KeyT extends keyof Slots>(container, id: KeyT): Slots[KeyT] { | |
export function GetSlot<KeyT extends keyof Slots>(container: AnyTemporalType, id: KeyT): Slots[KeyT] { |
return GetSlots(container)[id]; | ||
} | ||
export function SetSlot(container, id, value) { | ||
export function SetSlot<KeyT extends keyof Slots>(container: unknown, id: KeyT, value: Slots[KeyT]): void { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export function SetSlot<KeyT extends keyof Slots>(container: unknown, id: KeyT, value: Slots[KeyT]): void { | |
export function SetSlot<T extends AnyTemporalType, KeyT extends keyof Slots>( | |
container: T, | |
id: KeyT, | |
value: Slots[KeyT] | |
): void { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Justin has done a much better review of the TypeScript nitty gritty than I could, but I wanted to add an alternative that I'd been thinking about for a while. I believe the 'slots' mechanism was used in order to mimic the specification text which talks about the implementation's private values in terms of "internal slots". In the production polyfill we're not necessarily required to stick with this mechanism.
An alternative might be to make private "implementation" classes, something like this:
class PlainDateImpl {
#isoYear : number;
#isoMonth : number;
#isoDay : number;
#calendar : CalendarProtocol;
constructor(isoYear, isoMonth, isoDay, calendar) {
this.#isoYear = isoYear;
// etc
}
get isoYear() { return this.#isoYear; }
get isoMonth() { ... }
// etc
}
export class PlainDate {
#impl: PlainDateImpl;
constructor(isoYear, isoMonth, isoDay, calendar = undefined) {
this.#impl = new PlainDateImpl(isoYear, isoMonth, isoDay, calendar);
}
get year() {
return this.#impl.calendar.year(this);
}
// etc. - rest of public API goes here
}
(the private class would be necessary because other code has to access the private fields; but it would also allow optimizations such as changing functions such as DifferenceISODate which take loads of arguments, to actually be methods of PlainDateImpl instead.)
Anyway, a refactor like that would be much larger in scope than this change, but might be a good direction for changes like this to try to evolve into. I was wondering what the rest of you thought about it.
I think using private fields would be good at some point, but I'd worry that they're new enough that some downstream users may have trouble, even if they're polyfilled. Seems like it may make sense to wait a bit and refactor later. But I agree in general that this seems like a good direction to take. BTW, @12wrigja and I were chatting and we may end up merging a fork of this PR instead that does some other nifty things like making HasSlot into a type guard function so that |
Closing this PR as @justingrant merged in a better version of these changes in #74 . |
No description provided.