-
Notifications
You must be signed in to change notification settings - Fork 28
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
RFC: Fix null-check stuff #103
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.
How exciting to receive a PR from someone new to the project and eager to help, thanks very much!
I gave it a quick look and commented on two things that stood out to me. I can take a closer look again later, but it seems like adjusting the GetIntrinsic thing might change a lot of other stuff.
lib/plaindate.ts
Outdated
const Duration = GetIntrinsic('%Temporal.Duration%'); | ||
return new Duration(years, months, weeks, days, 0, 0, 0, 0, 0, 0); | ||
// const Duration = GetIntrinsic('%Temporal.Duration%')!; | ||
return new Temporal.Duration(years, months, weeks, days, 0, 0, 0, 0, 0, 0); |
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 we can't do, because it would break if user code monkey-patched globalThis.Temporal.Duration
. (That's the reason for the GetIntrinsic mechanism appearing all over the place, so that we can continue to use the original values internally even if they are replaced in user code.) Sorry, this should've been documented somewhere more clearly! Probably in intrinsic.ts
.
I think the best solution here (and elsewhere in the file) would be to type GetIntrinsic such that TypeScript knows that it returns Temporal.Duration for an argument of %Temporal.Duration%
. Unfortunately I don't immediately have an idea of how to do that properly 😄
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.
Could this be avoided by just doing import { Duration } from './duration'
? Or am I missing something? 😅
Makes sense otherwise, if importing is not sensible here I'll try to teach TypeScript how GetIntrinsic
works 😊
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 don't think we can import as it causes circular value dependencies between the different files? If that would work that could be one potential fix.
If we need to stick with GetIntrinsic
it should already be typed such that a key of %Temporal.Duration%
returns typeof Temporal.Duration
, but what probably needs to change is that the return type for GetIntrinsic needs to be NonNullable<typeof INTRINSICS[key]>
here.
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.
Circular dependencies should be fine as there's already ecmascript -> calendar -> ecmascript :D
If just importing is an option that might be cleaner, right?
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.
How does TypeScript usually handle circular module dependencies?
lib/slots.ts
Outdated
@@ -275,9 +275,10 @@ export function HasSlot(container: unknown, ...ids: (keyof Slots)[]): boolean { | |||
return !!myslots && ids.reduce((all: boolean, id) => all && id in myslots, true); | |||
} | |||
export function GetSlot<KeyT extends keyof Slots>( | |||
container: Slots[typeof id]['usedBy'], | |||
container: Slots[typeof id]['usedBy'] | undefined, |
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 it signifies a bug in the polyfill if we end up calling GetSlot(undefined, ...)
so I'd be happy if TS would complain at compile time rather than resorting to a runtime error. I wonder if there's any way to do that?
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.
In this case it can be undefined
if an incorrect disambiguation is given to BuiltinTimeZoneGetInstantFor
, so maybe it would be best to just throw?
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.
Maybe we can tighten the types in BuiltinTimeZoneGetInstantFor to ensure at compile time that no incorrect disambiguation can be given there? Or otherwise throw earlier if that happens?
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.
Seems like there may be a legit issue with DisambiguatePossibleInstants
because the spec says there should be an assertion that the disambiguation
value is valid but the polyfill lacks this assertion. We could add a line at the end of that method e.g.
throw new Error(`assertion failed: invalid disambiguation value ${disambiguation}`);
A quick drive-by comment: it is probably easiest to fix these flags by starting at the "root-most" files and working outwards. In this project, that would be in slots.ts and ecmascript.ts. Even small changes here will "ripple" through the rest of the files and might mean other fixes are necessary or obsolete. |
Yeah, that makes sense. |
On second thought, I think probably the two hardest files to change (but definitely most useful...) would be slots.ts and intrinsicclass.ts, as that is where a good chunk of the "type magic" is needed (places where TS can't easily infer the right typing). I'd suggest ecmascript.ts as third. |
I tried removing
|
If GetIntrinsic works, that might indeed be cleaner! But I'd be concerned, does it still work when bundling for a platform that doesn't have ES6 modules? I'd also suggest verifying that it really is robust in the face of monkey-patching. |
I haven't looked that deeply into the Rollup config used here, but as long as Rollup is properly configured, yes (and there seems to be a CommonJS export generated here, so we should be good to go).
To be honest I never heard of monkey-patching before, so I might have to do a bit of reading first 🙈 |
@@ -5,3 +5,4 @@ tsc-out/ | |||
.vscode/* | |||
!.vscode/launch.json | |||
*.tgz | |||
.DS_STORE |
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.
Needs a newline at the end of this line. I think npm run fix
should automatically add it. If it doesn't, then you might want to tweak the npm run fix
command line args. 😄
@@ -117,7 +118,7 @@ export class Calendar implements Temporal.Calendar { | |||
return ES.ToString(this); | |||
} | |||
dateFromFields( | |||
fields: Params['dateFromFields'][0], | |||
fields?: Params['dateFromFields'][0], |
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.
fields
should never be undefined.
Also, if undefined
were acceptable, then for any public API (like this one) it's intentional to leave it as Params...
because that enforces alignment between the implementation and index.d.ts. So let's imagine that undefined were OK for this API, then the right fix would be to change index.d.ts, not here.
@@ -178,7 +179,7 @@ export class Calendar implements Temporal.Calendar { | |||
if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver'); | |||
const date = ES.ToTemporalDate(dateParam); | |||
const duration = ES.ToTemporalDuration(durationParam); | |||
const options = ES.GetOptionsObject(optionsParam); | |||
const options = ES.GetOptionsObject<Temporal.ArithmeticOptions>(optionsParam); |
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.
Why is this explicit type parameter needed? Can't the type be inferred by TS from the type of options
?
@@ -26,6 +25,7 @@ import { | |||
PlainTimeParams, | |||
PlainYearMonthParams | |||
} from './internaltypes'; | |||
import { PlainDateTime } from './plaindatetime'; |
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.
Does this relative path need to be './plaindatetime.js' because of TS's relatively recent support for Node.js Experimental Modules? From https://www.typescriptlang.org/docs/handbook/esm-node.html#type-in-packagejson-and-new-extensions:
relative import paths need full extensions (we have to write import
"./foo.js"
instead of import"./foo"
)
Same applies to all use of imports like this.
@@ -1069,8 +1077,8 @@ export function ToPartialRecord<B extends AnyTemporalLikeType>( | |||
} | |||
|
|||
export function PrepareTemporalFields<B extends AnyTemporalLikeType>( | |||
bag: B, | |||
fields: ReadonlyArray<FieldRecord<B>> | |||
bag?: B, |
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 is probably the wrong solution. See tc39/proposal-temporal#1963.
} | ||
|
||
export function ComparisonResult(value: number) { | ||
return value < 0 ? -1 : value > 0 ? 1 : (value as 0); | ||
} | ||
|
||
export function GetOptionsObject<T>(options: T) { | ||
export function GetOptionsObject<T>(options: T | undefined) { |
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.
Could you explain why this change is needed? If undefined
is missing from the caller's type, then the fix is probably needed in the caller, not here. By adding undefined
here we're probably hiding a type bug in the caller.
@@ -134,6 +134,3 @@ export function DefineIntrinsic<KeyT extends IntrinsicDefinitionKeys>(name: KeyT | |||
if (INTRINSICS[key] !== undefined) throw new Error(`intrinsic ${name} already exists`); | |||
INTRINSICS[key] = value; | |||
} | |||
export function GetIntrinsic<KeyT extends keyof typeof INTRINSICS>(intrinsic: KeyT): typeof INTRINSICS[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.
Not sure this can be completely removed, even if the imports work, because there are some objects and methods (e.g. from
methods, maybe intrinsic prototypes) that still need to be accessed.
@@ -351,6 +350,7 @@ export class PlainDate implements Temporal.PlainDate { | |||
calendar | |||
); | |||
const instant = ES.BuiltinTimeZoneGetInstantFor(timeZone, dt, 'compatible'); | |||
if (!instant) throw new TypeError(`Could not create instant for timezone "${timeZone}" and dateTime "${dt}"`); |
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.
Can instant
actually be undefined here? I don't think it can be. The type problem is upstream. For example, DisambiguatePossibleInstants
needs an assertion at the end to prevent undefined
from being returned, e.g.
throw new Error(`assertion failed: invalid disambiguation value ${disambiguation}`);
lib/slots.ts
Outdated
@@ -275,9 +275,10 @@ export function HasSlot(container: unknown, ...ids: (keyof Slots)[]): boolean { | |||
return !!myslots && ids.reduce((all: boolean, id) => all && id in myslots, true); | |||
} | |||
export function GetSlot<KeyT extends keyof Slots>( | |||
container: Slots[typeof id]['usedBy'], | |||
container: Slots[typeof id]['usedBy'] | undefined, |
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.
Seems like there may be a legit issue with DisambiguatePossibleInstants
because the spec says there should be an assertion that the disambiguation
value is valid but the polyfill lacks this assertion. We could add a line at the end of that method e.g.
throw new Error(`assertion failed: invalid disambiguation value ${disambiguation}`);
Yep, there are a few possible issues. I'm probably missing some, but here's a few things to look at:
The first two bullet points above are covered here: tc39/proposal-temporal#1965 @jens-ox thanks so much for your work on this PR and in uncovering questions like this! |
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.
Thanks @jens-ox! Lots of notes are inline. You've already helped us find a few issues which is great. Some general feedback:
- We'll need to figure out the monkeypatching thing before we know if we can really get rid of GetIntrinsic or not.
- I agree with @12wrigja that it'd be best to start upstream with ecmascript.ts, slots, and intrinsics. In general, before you make a change to a method, consider whether the right change to make is upstream from it.
- @12wrigja and I can help! Especially with the upstream parts, there's lots of confusing parts of this polyfill. So feel free to tag us with questions if you get stuck.
- In general, you should try really hard to avoid runtime behavior changes (e.g. setting default parameters) to work around type issues. There's usually a way to solve those upstream.
- Also, adding nullability to parameters usually is incorrect; the problem is usually upstream.
- We'll certainly find problems or questions about the proposal-temporal polyfill and/or spec and/or TS types, etc. Examples that I've already found: Polyfill vs. spec mismatch in PrepareTemporalFields tc39/proposal-temporal#1963,
prototype
property of Temporal types should not be writable tc39/proposal-temporal#1965, Fix TS types for required CalendarProtocol methods tc39/proposal-temporal#1964. So this PR has already been helpful!
Hi @justingrant! Thanks for your comments. I opened #105 for the |
I think this PR has been replaced by others, so I'm going to close it now. If that's a mistake, feel free to re-open. |
Hi all!
As mentioned in #98, there are currently quite a few alerts when enabling strict null checks etc in the tsconfig. If it makes sense I'd really like to help, but I'm not sure what the preferred way of handling undefined stuff is.
This PR fixes all TS issues in
plainDate.ts
when turning on the all the currently commented-out flags in the tsconfig. In most cases, the only change is to allow methods that are called with possibly undefined values to handle that gracefully. Or do you prefer to throw more often if something is undefined?If this looks ok to you I can start fixing the remaining TS issues.