Skip to content

Commit 4423c5e

Browse files
committed
👍 Add isOptionalOf
1 parent 1152f24 commit 4423c5e

File tree

2 files changed

+82
-0
lines changed

2 files changed

+82
-0
lines changed

is.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,27 @@ export function isOneOf<T extends readonly Predicate<unknown>[]>(
273273
return (x: unknown): x is OneOf<T> => preds.some((pred) => pred(x));
274274
}
275275

276+
export type OptionalPredicate<T> = Predicate<T | undefined>;
277+
278+
/**
279+
* Return a type predicate function that returns `true` if the type of `x` is `T` or `undefined`.
280+
*
281+
* ```ts
282+
* import is from "./is.ts";
283+
*
284+
* const a: unknown = "a";
285+
* if (is.OptionalOf(is.String)(a)) {
286+
* // a is narrowed to string | undefined;
287+
* const _: string | undefined = a;
288+
* }
289+
* ```
290+
*/
291+
export function isOptionalOf<T>(
292+
pred: Predicate<T>,
293+
): OptionalPredicate<T> {
294+
return isOneOf([isUndefined, pred]);
295+
}
296+
276297
export default {
277298
String: isString,
278299
Number: isNumber,
@@ -292,4 +313,5 @@ export default {
292313
Nullish: isNullish,
293314
Symbol: isSymbol,
294315
OneOf: isOneOf,
316+
OptionalOf: isOptionalOf,
295317
};

is_test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import is, {
1818
isNumber,
1919
isObjectOf,
2020
isOneOf,
21+
isOptionalOf,
2122
isRecord,
2223
isRecordOf,
2324
isString,
@@ -346,6 +347,65 @@ Deno.test("isOneOf<T>", async (t) => {
346347
});
347348
});
348349

350+
Deno.test("isOptionalOf<T>", async (t) => {
351+
await t.step("returns proper type predicate", () => {
352+
const a: unknown = undefined;
353+
if (isOptionalOf(isNumber)(a)) {
354+
type _ = AssertTrue<IsExact<typeof a, number | undefined>>;
355+
}
356+
});
357+
await t.step("with isString", async (t) => {
358+
await testWithExamples(t, isOptionalOf(isString), {
359+
validExamples: ["string", "undefined"],
360+
});
361+
});
362+
await t.step("with isNumber", async (t) => {
363+
await testWithExamples(t, isOptionalOf(isNumber), {
364+
validExamples: ["number", "undefined"],
365+
});
366+
});
367+
await t.step("with isBigInt", async (t) => {
368+
await testWithExamples(t, isOptionalOf(isBigInt), {
369+
validExamples: ["bigint", "undefined"],
370+
});
371+
});
372+
await t.step("with isBoolean", async (t) => {
373+
await testWithExamples(t, isOptionalOf(isBoolean), {
374+
validExamples: ["boolean", "undefined"],
375+
});
376+
});
377+
await t.step("with isArray", async (t) => {
378+
await testWithExamples(t, isOptionalOf(isArray), {
379+
validExamples: ["array", "undefined"],
380+
});
381+
});
382+
await t.step("with isRecord", async (t) => {
383+
await testWithExamples(t, isOptionalOf(isRecord), {
384+
validExamples: ["record", "date", "promise", "undefined"],
385+
});
386+
});
387+
await t.step("with isFunction", async (t) => {
388+
await testWithExamples(t, isOptionalOf(isFunction), {
389+
validExamples: ["function", "undefined"],
390+
});
391+
});
392+
await t.step("with isNull", async (t) => {
393+
await testWithExamples(t, isOptionalOf(isNull), {
394+
validExamples: ["null", "undefined"],
395+
});
396+
});
397+
await t.step("with isUndefined", async (t) => {
398+
await testWithExamples(t, isOptionalOf(isUndefined), {
399+
validExamples: ["undefined"],
400+
});
401+
});
402+
await t.step("with isSymbol", async (t) => {
403+
await testWithExamples(t, isOptionalOf(isSymbol), {
404+
validExamples: ["symbol", "undefined"],
405+
});
406+
});
407+
});
408+
349409
Deno.test("is", async (t) => {
350410
const mod = await import("./is.ts");
351411
const casesOfAliasAndIsFunction = Object.entries(mod)

0 commit comments

Comments
 (0)