Skip to content

Commit 06b806c

Browse files
authored
Make runtime type checks TS-friendly (#73)
This commit makes minor refactors to runtime type checks to make that validation more TS-friendly: * Replace `Type(x)` for validating `Object` with a type guard `IsObject` function. The function is overloaded so unknown values are typed as `Record`, while known types like `DurationLike | string` are simply stripped of primitive types while leaving the object types. As a nice side effect, this should reduce bundle size a little bit. * For the other uses of ES.Type (there were <5 of them) , convert to simple typeof checks, which are also TS type guards. * Remove the ES.Type function * Add type annotations to make all IsTemporalXxx into TS type guards
1 parent 27b4c7e commit 06b806c

File tree

9 files changed

+61
-71
lines changed

9 files changed

+61
-71
lines changed

lib/calendar.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,19 @@ export class Calendar implements Temporal.Calendar {
6262
}
6363
dateFromFields(fields, options = undefined) {
6464
if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver');
65-
if (ES.Type(fields) !== 'Object') throw new TypeError('invalid fields');
65+
if (!ES.IsObject(fields)) throw new TypeError('invalid fields');
6666
options = ES.GetOptionsObject(options);
6767
return impl[GetSlot(this, CALENDAR_ID)].dateFromFields(fields, options, this);
6868
}
6969
yearMonthFromFields(fields, options = undefined) {
7070
if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver');
71-
if (ES.Type(fields) !== 'Object') throw new TypeError('invalid fields');
71+
if (!ES.IsObject(fields)) throw new TypeError('invalid fields');
7272
options = ES.GetOptionsObject(options);
7373
return impl[GetSlot(this, CALENDAR_ID)].yearMonthFromFields(fields, options, this);
7474
}
7575
monthDayFromFields(fields, options = undefined) {
7676
if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver');
77-
if (ES.Type(fields) !== 'Object') throw new TypeError('invalid fields');
77+
if (!ES.IsObject(fields)) throw new TypeError('invalid fields');
7878
options = ES.GetOptionsObject(options);
7979
return impl[GetSlot(this, CALENDAR_ID)].monthDayFromFields(fields, options, this);
8080
}
@@ -94,7 +94,7 @@ export class Calendar implements Temporal.Calendar {
9494
'nanosecond'
9595
]);
9696
for (const name of fields) {
97-
if (ES.Type(name) !== 'String') throw new TypeError('invalid fields');
97+
if (typeof name !== 'string') throw new TypeError('invalid fields');
9898
if (!allowed.has(name)) throw new RangeError(`invalid field name ${name}`);
9999
allowed.delete(name);
100100
ArrayPrototypePush.call(fieldsArray, name);
@@ -658,7 +658,7 @@ const nonIsoHelperBase: NonIsoHelperBase = {
658658
if (day === undefined) throw new RangeError('Missing day');
659659
if (monthCode !== undefined) {
660660
if (typeof monthCode !== 'string') {
661-
throw new RangeError(`monthCode must be a string, not ${ES.Type(monthCode).toLowerCase()}`);
661+
throw new RangeError(`monthCode must be a string, not ${typeof monthCode}`);
662662
}
663663
if (!/^M([01]?\d)(L?)$/.test(monthCode)) throw new RangeError(`Invalid monthCode: ${monthCode}`);
664664
}

lib/ecmascript.ts

Lines changed: 44 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const ReflectApply = Reflect.apply;
1818
import { DEBUG } from './debug';
1919
import bigInt from 'big-integer';
2020

21+
import type { Temporal } from '..';
2122
import { GetIntrinsic } from './intrinsicclass';
2223
import {
2324
CreateSlots,
@@ -68,25 +69,14 @@ function IsInteger(value: unknown): value is number {
6869
return MathFloor(abs) === abs;
6970
}
7071

71-
export function Type(value: unknown): string {
72-
if (value === null) return 'Null';
73-
switch (typeof value) {
74-
case 'symbol':
75-
return 'Symbol';
76-
case 'bigint':
77-
return 'BigInt';
78-
case 'undefined':
79-
return 'undefined';
80-
case 'function':
81-
case 'object':
82-
return 'Object';
83-
case 'number':
84-
return 'Number';
85-
case 'boolean':
86-
return 'Boolean';
87-
case 'string':
88-
return 'String';
89-
}
72+
// For unknown values, this narrows the result to a Record. But for union types
73+
// like `Temporal.DurationLike | string`, it'll strip the primitive types while
74+
// leaving the object type(s) unchanged.
75+
export function IsObject<T>(
76+
value: T
77+
): value is Exclude<T, string | null | undefined | number | bigint | symbol | boolean>;
78+
export function IsObject(value: unknown): value is Record<string | number | symbol, unknown> {
79+
return (typeof value === 'object' && value !== null) || typeof value === 'function';
9080
}
9181

9282
export function ToNumber(value: unknown): number {
@@ -109,16 +99,16 @@ export function ToString(value: unknown): string {
10999
return String(value);
110100
}
111101

112-
export function ToIntegerThrowOnInfinity(value) {
102+
export function ToIntegerThrowOnInfinity(value): number {
113103
const integer = ToInteger(value);
114104
if (!NumberIsFinite(integer)) {
115105
throw new RangeError('infinity is out of range');
116106
}
117107
return integer;
118108
}
119109

120-
export function ToPositiveInteger(value, property?: string) {
121-
value = ToInteger(value);
110+
export function ToPositiveInteger(valueParam: unknown, property?: string): number {
111+
const value = ToInteger(valueParam);
122112
if (!NumberIsFinite(value)) {
123113
throw new RangeError('infinity is out of range');
124114
}
@@ -131,15 +121,15 @@ export function ToPositiveInteger(value, property?: string) {
131121
return value;
132122
}
133123

134-
function ToIntegerNoFraction(value) {
135-
value = ToNumber(value);
124+
function ToIntegerNoFraction(valueParam: unknown): number {
125+
const value = ToNumber(valueParam);
136126
if (!IsInteger(value)) {
137127
throw new RangeError(`unsupported fractional value ${value}`);
138128
}
139129
return value;
140130
}
141131

142-
const BUILTIN_CASTS = new Map([
132+
const BUILTIN_CASTS = new Map<string, (v: unknown) => number | string>([
143133
['year', ToIntegerThrowOnInfinity],
144134
['month', ToPositiveInteger],
145135
['monthCode', ToString],
@@ -213,29 +203,29 @@ function getIntlDateTimeFormatEnUsForTimeZone(timeZoneIdentifier) {
213203
return instance;
214204
}
215205

216-
export function IsTemporalInstant(item) {
206+
export function IsTemporalInstant(item: unknown): item is Temporal.Instant {
217207
return HasSlot(item, EPOCHNANOSECONDS) && !HasSlot(item, TIME_ZONE, CALENDAR);
218208
}
219209

220-
export function IsTemporalTimeZone(item) {
210+
export function IsTemporalTimeZone(item: unknown): item is Temporal.TimeZone {
221211
return HasSlot(item, TIMEZONE_ID);
222212
}
223-
export function IsTemporalCalendar(item) {
213+
export function IsTemporalCalendar(item: unknown): item is Temporal.Calendar {
224214
return HasSlot(item, CALENDAR_ID);
225215
}
226-
export function IsTemporalDuration(item) {
216+
export function IsTemporalDuration(item: unknown): item is Temporal.Duration {
227217
return HasSlot(item, YEARS, MONTHS, DAYS, HOURS, MINUTES, SECONDS, MILLISECONDS, MICROSECONDS, NANOSECONDS);
228218
}
229-
export function IsTemporalDate(item) {
219+
export function IsTemporalDate(item: unknown): item is Temporal.PlainDate {
230220
return HasSlot(item, DATE_BRAND);
231221
}
232-
export function IsTemporalTime(item) {
222+
export function IsTemporalTime(item: unknown): item is Temporal.PlainTime {
233223
return (
234224
HasSlot(item, ISO_HOUR, ISO_MINUTE, ISO_SECOND, ISO_MILLISECOND, ISO_MICROSECOND, ISO_NANOSECOND) &&
235225
!HasSlot(item, ISO_YEAR, ISO_MONTH, ISO_DAY)
236226
);
237227
}
238-
export function IsTemporalDateTime(item) {
228+
export function IsTemporalDateTime(item: unknown): item is Temporal.PlainDateTime {
239229
return HasSlot(
240230
item,
241231
ISO_YEAR,
@@ -249,13 +239,13 @@ export function IsTemporalDateTime(item) {
249239
ISO_NANOSECOND
250240
);
251241
}
252-
export function IsTemporalYearMonth(item) {
242+
export function IsTemporalYearMonth(item: unknown): item is Temporal.PlainYearMonth {
253243
return HasSlot(item, YEAR_MONTH_BRAND);
254244
}
255-
export function IsTemporalMonthDay(item) {
245+
export function IsTemporalMonthDay(item: unknown): item is Temporal.PlainMonthDay {
256246
return HasSlot(item, MONTH_DAY_BRAND);
257247
}
258-
export function IsTemporalZonedDateTime(item) {
248+
export function IsTemporalZonedDateTime(item: unknown): item is Temporal.ZonedDateTime {
259249
return HasSlot(item, EPOCHNANOSECONDS, TIME_ZONE, CALENDAR);
260250
}
261251
function TemporalTimeZoneFromString(stringIdent) {
@@ -606,7 +596,7 @@ function ToTemporalDurationRecord(item) {
606596

607597
export function ToLimitedTemporalDuration(item, disallowedProperties = []) {
608598
let record;
609-
if (Type(item) === 'Object') {
599+
if (IsObject(item)) {
610600
record = ToTemporalDurationRecord(item);
611601
} else {
612602
const str = ToString(item);
@@ -711,7 +701,7 @@ export function ToSecondsStringPrecision(options): {
711701
}
712702
let digits = options.fractionalSecondDigits;
713703
if (digits === undefined) digits = 'auto';
714-
if (Type(digits) !== 'Number') {
704+
if (typeof digits !== 'number') {
715705
digits = ToString(digits);
716706
if (digits === 'auto') return { precision: 'auto', unit: 'nanosecond', increment: 1 };
717707
throw new RangeError(`fractionalSecondDigits must be 'auto' or 0 through 9, not ${digits}`);
@@ -780,7 +770,7 @@ export function ToRelativeTemporalObject(options) {
780770

781771
let offsetBehaviour = 'option';
782772
let year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, timeZone, offset;
783-
if (Type(relativeTo) === 'Object') {
773+
if (IsObject(relativeTo)) {
784774
if (IsTemporalZonedDateTime(relativeTo) || IsTemporalDateTime(relativeTo)) return relativeTo;
785775
if (IsTemporalDate(relativeTo)) {
786776
return CreateTemporalDateTime(
@@ -889,7 +879,7 @@ export function LargerOfTwoTemporalUnits(unit1, unit2) {
889879
}
890880

891881
export function ToPartialRecord(bag, fields, callerCast?: (value: unknown) => unknown) {
892-
if (Type(bag) !== 'Object') return false;
882+
if (!IsObject(bag)) return false;
893883
let any;
894884
for (const property of fields) {
895885
const value = bag[property];
@@ -908,7 +898,7 @@ export function ToPartialRecord(bag, fields, callerCast?: (value: unknown) => un
908898
}
909899

910900
export function PrepareTemporalFields(bag, fields) {
911-
if (Type(bag) !== 'Object') return undefined;
901+
if (!IsObject(bag)) return undefined;
912902
const result = {};
913903
let any = false;
914904
for (const fieldRecord of fields) {
@@ -1042,7 +1032,7 @@ export function ToTemporalZonedDateTimeFields(bag, fieldNames) {
10421032
}
10431033

10441034
export function ToTemporalDate(item, options = ObjectCreate(null)) {
1045-
if (Type(item) === 'Object') {
1035+
if (IsObject(item)) {
10461036
if (IsTemporalDate(item)) return item;
10471037
if (IsTemporalZonedDateTime(item)) {
10481038
item = BuiltinTimeZoneGetPlainDateTimeFor(
@@ -1091,7 +1081,7 @@ export function InterpretTemporalDateTimeFields(calendar, fields, options) {
10911081

10921082
export function ToTemporalDateTime(item, options = ObjectCreate(null)) {
10931083
let year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar;
1094-
if (Type(item) === 'Object') {
1084+
if (IsObject(item)) {
10951085
if (IsTemporalDateTime(item)) return item;
10961086
if (IsTemporalZonedDateTime(item)) {
10971087
return BuiltinTimeZoneGetPlainDateTimeFor(
@@ -1147,7 +1137,7 @@ export function ToTemporalDateTime(item, options = ObjectCreate(null)) {
11471137

11481138
export function ToTemporalDuration(item) {
11491139
let years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds;
1150-
if (Type(item) === 'Object') {
1140+
if (IsObject(item)) {
11511141
if (IsTemporalDuration(item)) return item;
11521142
({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } =
11531143
ToTemporalDurationRecord(item));
@@ -1182,7 +1172,7 @@ export function ToTemporalInstant(item) {
11821172
}
11831173

11841174
export function ToTemporalMonthDay(item, options = ObjectCreate(null)) {
1185-
if (Type(item) === 'Object') {
1175+
if (IsObject(item)) {
11861176
if (IsTemporalMonthDay(item)) return item;
11871177
let calendar, calendarAbsent;
11881178
if (HasSlot(item, CALENDAR)) {
@@ -1221,7 +1211,7 @@ export function ToTemporalMonthDay(item, options = ObjectCreate(null)) {
12211211

12221212
export function ToTemporalTime(item, overflow = 'constrain') {
12231213
let hour, minute, second, millisecond, microsecond, nanosecond, calendar;
1224-
if (Type(item) === 'Object') {
1214+
if (IsObject(item)) {
12251215
if (IsTemporalTime(item)) return item;
12261216
if (IsTemporalZonedDateTime(item)) {
12271217
item = BuiltinTimeZoneGetPlainDateTimeFor(
@@ -1269,7 +1259,7 @@ export function ToTemporalTime(item, overflow = 'constrain') {
12691259
}
12701260

12711261
export function ToTemporalYearMonth(item, options = ObjectCreate(null)) {
1272-
if (Type(item) === 'Object') {
1262+
if (IsObject(item)) {
12731263
if (IsTemporalYearMonth(item)) return item;
12741264
const calendar = GetTemporalCalendarWithISODefault(item);
12751265
const fieldNames = CalendarFields(calendar, ['month', 'monthCode', 'year']);
@@ -1353,7 +1343,7 @@ export function InterpretISODateTimeOffset(
13531343
export function ToTemporalZonedDateTime(item, options = ObjectCreate(null)) {
13541344
let year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, timeZone, offset, calendar;
13551345
let offsetBehaviour = 'option';
1356-
if (Type(item) === 'Object') {
1346+
if (IsObject(item)) {
13571347
if (IsTemporalZonedDateTime(item)) return item;
13581348
calendar = GetTemporalCalendarWithISODefault(item);
13591349
const fieldNames = CalendarFields(calendar, [
@@ -1588,7 +1578,7 @@ export function CalendarFields(calendar, fieldNames) {
15881578
}
15891579
const result = [];
15901580
for (const name of fieldNames) {
1591-
if (Type(name) !== 'String') throw new TypeError('bad return from calendar.fields()');
1581+
if (typeof name !== 'string') throw new TypeError('bad return from calendar.fields()');
15921582
ArrayPrototypePush.call(result, name);
15931583
}
15941584
return result;
@@ -1600,7 +1590,7 @@ export function CalendarMergeFields(calendar, fields, additionalFields) {
16001590
return { ...fields, ...additionalFields };
16011591
}
16021592
const result = Reflect.apply(calMergeFields, calendar, [fields, additionalFields]);
1603-
if (Type(result) !== 'Object') throw new TypeError('bad return from calendar.mergeFields()');
1593+
if (!IsObject(result)) throw new TypeError('bad return from calendar.mergeFields()');
16041594
return result;
16051595
}
16061596

@@ -1703,11 +1693,11 @@ export function CalendarInLeapYear(calendar, dateLike) {
17031693
}
17041694

17051695
export function ToTemporalCalendar(calendarLike) {
1706-
if (Type(calendarLike) === 'Object') {
1696+
if (IsObject(calendarLike)) {
17071697
if (HasSlot(calendarLike, CALENDAR)) return GetSlot(calendarLike, CALENDAR);
17081698
if (!('calendar' in calendarLike)) return calendarLike;
17091699
calendarLike = calendarLike.calendar;
1710-
if (Type(calendarLike) === 'Object' && !('calendar' in calendarLike)) return calendarLike;
1700+
if (IsObject(calendarLike) && !('calendar' in calendarLike)) return calendarLike;
17111701
}
17121702
const identifier = ToString(calendarLike);
17131703
const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%');
@@ -1768,11 +1758,11 @@ export function MonthDayFromFields(calendar, fields, options?: any) {
17681758
}
17691759

17701760
export function ToTemporalTimeZone(temporalTimeZoneLike) {
1771-
if (Type(temporalTimeZoneLike) === 'Object') {
1761+
if (IsObject(temporalTimeZoneLike)) {
17721762
if (IsTemporalZonedDateTime(temporalTimeZoneLike)) return GetSlot(temporalTimeZoneLike, TIME_ZONE);
17731763
if (!('timeZone' in temporalTimeZoneLike)) return temporalTimeZoneLike;
17741764
temporalTimeZoneLike = temporalTimeZoneLike.timeZone;
1775-
if (Type(temporalTimeZoneLike) === 'Object' && !('timeZone' in temporalTimeZoneLike)) {
1765+
if (IsObject(temporalTimeZoneLike) && !('timeZone' in temporalTimeZoneLike)) {
17761766
return temporalTimeZoneLike;
17771767
}
17781768
}
@@ -4296,7 +4286,7 @@ export function ComparisonResult(value) {
42964286
}
42974287
export function GetOptionsObject(options) {
42984288
if (options === undefined) return ObjectCreate(null);
4299-
if (Type(options) === 'Object') return options;
4289+
if (IsObject(options)) return options;
43004290
throw new TypeError(`Options parameter must be an object, not ${options === null ? 'null' : `a ${typeof options}`}`);
43014291
}
43024292

lib/instant.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ export class Instant implements Temporal.Instant {
213213
}
214214
toZonedDateTime(item) {
215215
if (!ES.IsTemporalInstant(this)) throw new TypeError('invalid receiver');
216-
if (ES.Type(item) !== 'Object') {
216+
if (!ES.IsObject(item)) {
217217
throw new TypeError('invalid argument in toZonedDateTime');
218218
}
219219
const calendarLike = item.calendar;
@@ -230,7 +230,7 @@ export class Instant implements Temporal.Instant {
230230
}
231231
toZonedDateTimeISO(item) {
232232
if (!ES.IsTemporalInstant(this)) throw new TypeError('invalid receiver');
233-
if (ES.Type(item) === 'Object') {
233+
if (ES.IsObject(item)) {
234234
const timeZoneProperty = item.timeZone;
235235
if (timeZoneProperty !== undefined) {
236236
item = timeZoneProperty;

lib/plaindate.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export class PlainDate implements Temporal.PlainDate {
100100
}
101101
with(temporalDateLike, options = undefined) {
102102
if (!ES.IsTemporalDate(this)) throw new TypeError('invalid receiver');
103-
if (ES.Type(temporalDateLike) !== 'Object') {
103+
if (!ES.IsObject(temporalDateLike)) {
104104
throw new TypeError('invalid argument');
105105
}
106106
if (HasSlot(temporalDateLike, CALENDAR) || HasSlot(temporalDateLike, TIME_ZONE)) {
@@ -321,7 +321,7 @@ export class PlainDate implements Temporal.PlainDate {
321321
if (!ES.IsTemporalDate(this)) throw new TypeError('invalid receiver');
322322

323323
let timeZone, temporalTime;
324-
if (ES.Type(item) === 'Object') {
324+
if (ES.IsObject(item)) {
325325
const timeZoneLike = item.timeZone;
326326
if (timeZoneLike === undefined) {
327327
timeZone = ES.ToTemporalTimeZone(item);

lib/plaindatetime.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ export class PlainDateTime implements Temporal.PlainDateTime {
152152
}
153153
with(temporalDateTimeLike, options = undefined) {
154154
if (!ES.IsTemporalDateTime(this)) throw new TypeError('invalid receiver');
155-
if (ES.Type(temporalDateTimeLike) !== 'Object') {
155+
if (!ES.IsObject(temporalDateTimeLike)) {
156156
throw new TypeError('invalid argument');
157157
}
158158
if (HasSlot(temporalDateTimeLike, CALENDAR) || HasSlot(temporalDateTimeLike, TIME_ZONE)) {

0 commit comments

Comments
 (0)