Skip to content

Commit aa75726

Browse files
authored
Merge pull request #35 from lambdalisue/support-literal
Add `isPrimitive`, `isLiteralOf<T>`, and `isLiteralOneOf<T>` predicate/predicator functions
2 parents e00eee8 + 2edee43 commit aa75726

File tree

3 files changed

+151
-0
lines changed

3 files changed

+151
-0
lines changed

is.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,49 @@ export function isSymbol(x: unknown): x is symbol {
255255
return typeof x === "symbol";
256256
}
257257

258+
export type Primitive =
259+
| string
260+
| number
261+
| bigint
262+
| boolean
263+
| null
264+
| undefined
265+
| symbol;
266+
267+
/**
268+
* Return `true` if the type of `x` is Primitive.
269+
*/
270+
export function isPrimitive(x: unknown): x is Primitive {
271+
return x == null ||
272+
["string", "number", "bigint", "boolean", "symbol"].includes(typeof x);
273+
}
274+
275+
/**
276+
* Return a type predicate function that returns `true` if the type of `x` is a literal type of `pred`.
277+
*/
278+
export function isLiteralOf<T extends Primitive>(pred: T): Predicate<T> {
279+
return (x: unknown): x is T => x === pred;
280+
}
281+
282+
/**
283+
* Return a type predicate function that returns `true` if the type of `x` is one of literal type in `preds`.
284+
*
285+
* ```ts
286+
* import is from "./is.ts";
287+
* const a: unknown = "hello";
288+
* if (is.LiteralOneOf(["hello", "world"] as const)(a)) {
289+
* // a is narrowed to "hello" | "world"
290+
* const _: "hello" | "world" = a;
291+
* }
292+
* ```
293+
*/
294+
export function isLiteralOneOf<T extends readonly Primitive[]>(
295+
preds: T,
296+
): Predicate<T[number]> {
297+
return (x: unknown): x is T[number] =>
298+
preds.includes(x as unknown as T[number]);
299+
}
300+
258301
export type OneOf<T> = T extends Predicate<infer U>[] ? U : never;
259302

260303
/**
@@ -346,6 +389,9 @@ export default {
346389
Undefined: isUndefined,
347390
Nullish: isNullish,
348391
Symbol: isSymbol,
392+
Primitive: isPrimitive,
393+
LiteralOf: isLiteralOf,
394+
LiteralOneOf: isLiteralOneOf,
349395
OneOf: isOneOf,
350396
AllOf: isAllOf,
351397
OptionalOf: isOptionalOf,

is_bench.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,57 @@ Deno.bench({
246246
},
247247
});
248248

249+
Deno.bench({
250+
name: "is.Primitive",
251+
fn: () => {
252+
for (const c of cs) {
253+
is.Primitive(c);
254+
}
255+
},
256+
});
257+
258+
const predLiteral = "hello";
259+
Deno.bench({
260+
name: "is.LiteralOf",
261+
fn: () => {
262+
const pred = is.LiteralOf(predLiteral);
263+
for (const c of cs) {
264+
pred(c);
265+
}
266+
},
267+
});
268+
269+
const isLiteralOfPred = is.LiteralOf(predLiteral);
270+
Deno.bench({
271+
name: "is.LiteralOf (pre)",
272+
fn: () => {
273+
for (const c of cs) {
274+
isLiteralOfPred(c);
275+
}
276+
},
277+
});
278+
279+
const predLiteralOne = ["hello", "world"] as const;
280+
Deno.bench({
281+
name: "is.LiteralOneOf",
282+
fn: () => {
283+
const pred = is.LiteralOneOf(predLiteralOne);
284+
for (const c of cs) {
285+
pred(c);
286+
}
287+
},
288+
});
289+
290+
const isLiteralOneOfPred = is.LiteralOneOf(predLiteralOne);
291+
Deno.bench({
292+
name: "is.LiteralOneOf (pre)",
293+
fn: () => {
294+
for (const c of cs) {
295+
isLiteralOneOfPred(c);
296+
}
297+
},
298+
});
299+
249300
const predsOne = [is.String, is.Number, is.Boolean] as const;
250301
Deno.bench({
251302
name: "is.OneOf",

is_test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ import is, {
1414
isBoolean,
1515
isFunction,
1616
isInstanceOf,
17+
isLiteralOf,
18+
isLiteralOneOf,
1719
isNull,
1820
isNullish,
1921
isNumber,
2022
isObjectOf,
2123
isOneOf,
2224
isOptionalOf,
25+
isPrimitive,
2326
isRecord,
2427
isRecordOf,
2528
isString,
@@ -421,6 +424,57 @@ Deno.test("isSymbol", async (t) => {
421424
await testWithExamples(t, isSymbol, { validExamples: ["symbol"] });
422425
});
423426

427+
Deno.test("isPrimitive", async (t) => {
428+
await testWithExamples(t, isPrimitive, {
429+
validExamples: [
430+
"string",
431+
"number",
432+
"bigint",
433+
"boolean",
434+
"null",
435+
"undefined",
436+
"symbol",
437+
],
438+
});
439+
});
440+
441+
Deno.test("isLiteralOf<T>", async (t) => {
442+
await t.step("returns proper type predicate", () => {
443+
const pred = "hello";
444+
const a: unknown = "hello";
445+
if (isLiteralOf(pred)(a)) {
446+
type _ = AssertTrue<IsExact<typeof a, "hello">>;
447+
}
448+
});
449+
await t.step("returns true on literal T", () => {
450+
const pred = "hello";
451+
assertEquals(isLiteralOf(pred)("hello"), true);
452+
});
453+
await t.step("returns false on non literal T", async (t) => {
454+
const pred = "hello";
455+
await testWithExamples(t, isLiteralOf(pred));
456+
});
457+
});
458+
459+
Deno.test("isLiteralOneOf<T>", async (t) => {
460+
await t.step("returns proper type predicate", () => {
461+
const preds = ["hello", "world"] as const;
462+
const a: unknown = "hello";
463+
if (isLiteralOneOf(preds)(a)) {
464+
type _ = AssertTrue<IsExact<typeof a, "hello" | "world">>;
465+
}
466+
});
467+
await t.step("returns true on literal T", () => {
468+
const preds = ["hello", "world"] as const;
469+
assertEquals(isLiteralOneOf(preds)("hello"), true);
470+
assertEquals(isLiteralOneOf(preds)("world"), true);
471+
});
472+
await t.step("returns false on non literal T", async (t) => {
473+
const preds = ["hello", "world"] as const;
474+
await testWithExamples(t, isLiteralOneOf(preds));
475+
});
476+
});
477+
424478
Deno.test("isOneOf<T>", async (t) => {
425479
await t.step("returns proper type predicate", () => {
426480
const preds = [isNumber, isString, isBoolean];

0 commit comments

Comments
 (0)