Skip to content

Add support for Temporal types#352

Open
SamPearlma wants to merge 4 commits intoflightcontrolhq:mainfrom
SamPearlma:main
Open

Add support for Temporal types#352
SamPearlma wants to merge 4 commits intoflightcontrolhq:mainfrom
SamPearlma:main

Conversation

@SamPearlma
Copy link
Copy Markdown

@SamPearlma SamPearlma commented Apr 15, 2026

Adds serialization support for all eight Temporal types: Instant,
Duration, PlainDate, PlainDateTime, PlainMonthDay, PlainTime,
PlainYearMonth, ZonedDateTime.

Values are encoded as their ISO / RFC 9557 format string form and restored via each
constructor's .from(). Temporal globals are referenced lazily, so
runtimes without Temporal are unaffected.

Also generalizes the test skipOnNode10 flag into a reusable skip
predicate to gate Temporal cases on runtime support.

Test plan

  • npm test passes
  • npm run test:temporal passes on a Temporal-capable Node
  • npm run build succeeds

Greptile Summary

Adds first-class serialization support for all eight TC39 Temporal types by introducing a lazy-initialized temporalRule in transformer.ts and corresponding isTemporal type guards in is.ts. Temporal globals are only accessed at call time, so runtimes without Temporal are unaffected. The test suite is well-structured with gated cases for all eight types, and the skip generalization cleanly replaces the old skipOnNode10 flag.

Confidence Score: 5/5

Safe to merge — all remaining findings are P2 style suggestions.

The core Temporal serialization/deserialization logic is correct: lazy init guard is safe for non-Temporal runtimes, toString()/from() round-trips are correct for all 8 types, annotation lookup via constructor name is consistent, and test gating with skip: !hasTemporal works correctly. The only open item is a P2 suggestion to use NODE_OPTIONS instead of a hardcoded internal vitest path.

package.json — test:temporal script uses a vitest-internal file path that could break on upgrade.

Important Files Changed

Filename Overview
src/transformer.ts Adds temporalRule using compositeTransformation with lazy-initialized constructor map; serializes via .toString(), deserializes via ctor.from(); ordering in compositeRules correctly places it after custom/class rules.
src/is.ts Adds isTemporal and per-type guards using Object.prototype.toString tag matching; safely returns false on non-Temporal runtimes since no Temporal global is referenced at runtime.
src/index.test.ts Generalises skipOnNode10 to skip predicate; adds 10 Temporal test cases all correctly guarded with skip: !hasTemporal; output expectations and custom assertions look correct for all 8 Temporal types.
src/is.test.ts Imports and tests isTemporal; false-path tests run unconditionally, true-path tests correctly gated with test.skipIf(typeof Temporal === 'undefined').
package.json Adds test:temporal script using a direct path to vitest internals (vitest.mjs) which is fragile across upgrades; TypeScript bumped to 6.0.2.
tsconfig.json Adds ignoreDeprecations: '6.0' for TypeScript 6 upgrade and types: ['node'] to restrict global type auto-inclusion; intentional to enable Temporal type support.
package-lock.json Lockfile updated to reflect TypeScript 6.0.2 and removal of some optional peer dependencies; no anomalies.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Input value] --> B{isTemporal?}
    B -- Yes --> C[annotation: temporal + constructor.name]
    C --> D[transform: t.toString()]
    D --> E[JSON string e.g. 2024-01-01T10:00:00Z]
    E --> F{untransformValue type === temporal?}
    F -- Yes --> G[getTemporalConstructors]
    G --> H{Temporal defined?}
    H -- No --> I[throw: Temporal not available]
    H -- Yes --> J[Lazy-init name to ctor map]
    J --> K[lookup ctor by annotation name]
    K --> L[ctor.from serializedString]
    L --> M[Restored Temporal instance]
    B -- No --> N[other rules / pass through]
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: package.json
Line: 19

Comment:
**`test:temporal` hardcodes vitest internals**

`./node_modules/vitest/vitest.mjs` couples the script to vitest's internal package layout. When vitest is upgraded this path could change silently. The standard pattern for passing Node.js flags through an npm script is `NODE_OPTIONS`:

```suggestion
    "test:temporal": "NODE_OPTIONS='--harmony-temporal' vitest run",
```

This also ensures the flag propagates to any child/worker processes that vitest may spawn, whereas the current approach relies on V8 flags being shared with worker-threads (which they are in the default `--threads` mode, but wouldn't be if `--pool=forks` is ever used).

How can I resolve this? If you propose a fix, please make it concise.

Reviews (2): Last reviewed commit: "fix: address scary Greptile style concer..." | Re-trigger Greptile

@SamPearlma SamPearlma requested a review from Skn0tt as a code owner April 15, 2026 17:32
Comment thread src/transformer.ts Outdated
Comment on lines +260 to +263
// Can not define temporalConstructorToName within global scope
// as Temporal may be undefined (don't introduce breaking changes).
// So lazily initialize array
let temporalConstructorToName: Record<string, TemporalConstructor> | undefined;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Inverted variable name and misleading comment

temporalConstructorToName maps name → constructor (a Record<string, TemporalConstructor>), which is the opposite of what the name implies. The comment above it also says "lazily initialize array" but this is a plain object/Record. A future reader looking for how a constructor maps to its name will be confused. Consider renaming to nameToTemporalConstructor (or just temporalConstructors) and updating the comment.

Suggested change
// Can not define temporalConstructorToName within global scope
// as Temporal may be undefined (don't introduce breaking changes).
// So lazily initialize array
let temporalConstructorToName: Record<string, TemporalConstructor> | undefined;
// Lazily initialized to avoid referencing Temporal at module load time on
// runtimes that don't support it.
let nameToTemporalConstructor: Record<string, TemporalConstructor> | undefined;
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/transformer.ts
Line: 260-263

Comment:
**Inverted variable name and misleading comment**

`temporalConstructorToName` maps *name → constructor* (a `Record<string, TemporalConstructor>`), which is the opposite of what the name implies. The comment above it also says "lazily initialize array" but this is a plain object/Record. A future reader looking for how a constructor maps to its name will be confused. Consider renaming to `nameToTemporalConstructor` (or just `temporalConstructors`) and updating the comment.

```suggestion
// Lazily initialized to avoid referencing Temporal at module load time on
// runtimes that don't support it.
let nameToTemporalConstructor: Record<string, TemporalConstructor> | undefined;
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mirrors earlier constructorToName

Comment thread src/transformer.ts
@SamPearlma
Copy link
Copy Markdown
Author

In case anyone comes across this without it being implemented, you can achieve the same functionality by doing

const temporalTypes = [
	Temporal.Instant,
	Temporal.ZonedDateTime,
	Temporal.PlainDateTime,
	Temporal.PlainDate,
	Temporal.PlainTime,
	Temporal.PlainYearMonth,
	Temporal.PlainMonthDay,
	Temporal.Duration,
] as const;

for (const Type of temporalTypes) {
	superjson.registerCustom<InstanceType<typeof Type>, string>(
		{
			isApplicable: (v): v is InstanceType<typeof Type> => v instanceof Type,
			serialize: (v) => v.toString(),
			deserialize: (v) => Type.from(v as never) as InstanceType<typeof Type>,
		},
		`Temporal.${Type.name}`,
	);
}

If you are sending between separate servers and clients, make sure you register the custom encoders on both

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant