From 40f5c8aa558020010f2778b08d58d746b24f45c8 Mon Sep 17 00:00:00 2001 From: Alisue Date: Tue, 13 Feb 2024 00:17:38 +0900 Subject: [PATCH] :+1: Add `isReadonlyOf` and deprecate `isReadonlyTupleOf/isReadonlyUniformTupleOf` --- is/__snapshots__/annotation_test.ts.snap | 18 +++ is/__snapshots__/factory_test.ts.snap | 176 +++++++++++------------ is/__snapshots__/utility_test.ts.snap | 36 ++--- is/annotation.ts | 69 +++++++++ is/annotation_test.ts | 36 ++++- is/factory.ts | 73 ++++------ 6 files changed, 258 insertions(+), 150 deletions(-) diff --git a/is/__snapshots__/annotation_test.ts.snap b/is/__snapshots__/annotation_test.ts.snap index f9401a0..b1b23e9 100644 --- a/is/__snapshots__/annotation_test.ts.snap +++ b/is/__snapshots__/annotation_test.ts.snap @@ -1,5 +1,23 @@ export const snapshot = {}; +snapshot[`isReadonlyOf > returns properly named function 1`] = ` +"isReadonlyOf(isTupleOf([ + isString, + isNumber, + isBoolean +]))" +`; + +snapshot[`isReadonlyOf > returns properly named function 2`] = `"isReadonlyOf(isUniformTupleOf(3, isString))"`; + +snapshot[`isReadonlyOf > returns properly named function 3`] = ` +"isReadonlyOf(isObjectOf({ + a: isString, + b: isNumber, + c: isBoolean +}))" +`; + snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; diff --git a/is/__snapshots__/factory_test.ts.snap b/is/__snapshots__/factory_test.ts.snap index 4fff689..4c5b70c 100644 --- a/is/__snapshots__/factory_test.ts.snap +++ b/is/__snapshots__/factory_test.ts.snap @@ -1,16 +1,8 @@ export const snapshot = {}; -snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; - -snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; - -snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; - -snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; - -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; snapshot[`isTupleOf > returns properly named function 1`] = ` "isTupleOf([ @@ -34,36 +26,14 @@ snapshot[`isTupleOf > returns properly named function 3`] = ` ])" `; -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; - -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; - -snapshot[`isTupleOf > returns properly named function 1`] = ` -"isTupleOf([ - isNumber, - isString, - isBoolean -], isArray)" -`; - -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; - -snapshot[`isTupleOf > returns properly named function 3`] = ` -"isTupleOf([ - isTupleOf([ - isTupleOf([ - isNumber, - isString, - isBoolean - ], isArray) - ], isArray) -])" -`; - snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; +snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; + +snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; + snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; @@ -78,8 +48,58 @@ snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(u snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; +snapshot[`isObjectOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean +})" +`; + +snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; + +snapshot[`isObjectOf > returns properly named function 3`] = ` +"isObjectOf({ + a: isObjectOf({ + b: isObjectOf({c: isBoolean}) + }) +})" +`; + +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyOf(isTupleOf([ + isNumber, + isString, + isBoolean +]))" +`; + +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyOf(isTupleOf([(anonymous)]))"`; + +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ + isNumber, + isString, + isBoolean + ])) + ])) +]))" +`; + +snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; + +snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; + snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; +snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; + +snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; + +snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; + snapshot[`isStrictOf > returns properly named function 1`] = ` "isStrictOf(isObjectOf({ a: isNumber, @@ -98,84 +118,64 @@ snapshot[`isStrictOf > returns properly named function 3`] = ` }))" `; +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; + +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; + +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; + +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; + snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyTupleOf([ +"isReadonlyOf(isTupleOf([ isNumber, isString, isBoolean -], isArray)" +], isArray))" `; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)], isArrayOf(isString))"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyOf(isTupleOf([(anonymous)], isArrayOf(isString)))"`; snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyTupleOf([ - isReadonlyTupleOf([ - isReadonlyTupleOf([ +"isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ isNumber, isString, isBoolean - ], isArray) - ], isArray) -], isArray)" + ], isArray)) + ], isArray)) +], isArray))" `; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyOf(isUniformTupleOf(3, isAny))"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyOf(isUniformTupleOf(3, isNumber))"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyOf(isUniformTupleOf(3, (anonymous)))"`; -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyTupleOf([ +snapshot[`isTupleOf > returns properly named function 1`] = ` +"isTupleOf([ isNumber, isString, isBoolean -])" +], isArray)" `; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)])"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyTupleOf([ - isReadonlyTupleOf([ - isReadonlyTupleOf([ +snapshot[`isTupleOf > returns properly named function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ isNumber, isString, isBoolean - ]) - ]) + ], isArray) + ], isArray) ])" `; - -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; - -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; - -snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; - -snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; - -snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; - -snapshot[`isObjectOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - b: isString, - c: isBoolean -})" -`; - -snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; - -snapshot[`isObjectOf > returns properly named function 3`] = ` -"isObjectOf({ - a: isObjectOf({ - b: isObjectOf({c: isBoolean}) - }) -})" -`; diff --git a/is/__snapshots__/utility_test.ts.snap b/is/__snapshots__/utility_test.ts.snap index 40c0897..1fff46d 100644 --- a/is/__snapshots__/utility_test.ts.snap +++ b/is/__snapshots__/utility_test.ts.snap @@ -1,5 +1,14 @@ export const snapshot = {}; +snapshot[`isOmitOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + c: isBoolean +})" +`; + +snapshot[`isOmitOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; + snapshot[`isUnionOf > returns properly named function 1`] = ` "isUnionOf([ isNumber, @@ -8,6 +17,15 @@ snapshot[`isUnionOf > returns properly named function 1`] = ` ])" `; +snapshot[`isPickOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + c: isBoolean +})" +`; + +snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; + snapshot[`isPartialOf > returns properly named function 1`] = ` "isObjectOf({ a: isOptionalOf(isNumber), @@ -24,27 +42,9 @@ snapshot[`isPartialOf > returns properly named function 2`] = ` })" `; -snapshot[`isOmitOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - c: isBoolean -})" -`; - -snapshot[`isOmitOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; - snapshot[`isIntersectionOf > returns properly named function 1`] = ` "isObjectOf({ a: isNumber, b: isString })" `; - -snapshot[`isPickOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - c: isBoolean -})" -`; - -snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; diff --git a/is/annotation.ts b/is/annotation.ts index cbc7abb..f037a4e 100644 --- a/is/annotation.ts +++ b/is/annotation.ts @@ -72,7 +72,76 @@ type IsOptionalOfMetadata = { args: Parameters; }; +/** + * Return `true` if the type of predicate `x` is `Readonly` predicate. + * + * ```ts + * import { is, type Predicate, type WithReadonly } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const pred: Predicate = is.ReadonlyOf(is.Unknown); + * if (is.Readonly(pred)) { + * // a is narrowed to Predicate & WithReadonly + * const _: Predicate & WithReadonly = pred; + * } + * ``` + */ +export function isReadonly

>( + x: P, +): x is P & WithReadonly { + return (x as Partial).readonly === true; +} + +/** + * Annotate the type predicate function as `Readonly`. + */ +export type WithReadonly = { + readonly: true; +}; + +/** + * Return an `Readonly` annotated type predicate function that returns `true` if the type of `x` is `T`. + * + * Note that the predicate function returned from this factory does nothing but annotate the types. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.ReadonlyOf(is.String); + * const a: unknown = "a"; + * if (isMyType(a)) { + * // a is narrowed to string | undefined + * const _: string | undefined = a; + * } + * ``` + */ +export function isReadonlyOf( + pred: Predicate, +): + & Predicate> + & WithReadonly + & WithMetadata { + return Object.defineProperties( + setPredicateFactoryMetadata( + (x: unknown): x is Readonly => pred(x), + { name: "isReadonlyOf", args: [pred] }, + ), + { readonly: { value: true as const } }, + ) as + & Predicate> + & WithReadonly + & WithMetadata; +} + +type IsReadonlyOfMetadata = { + name: "isReadonlyOf"; + args: Parameters; +}; + export default { Optional: isOptional, OptionalOf: isOptionalOf, + Readonly: isReadonly, + ReadonlyOf: isReadonlyOf, }; diff --git a/is/annotation_test.ts b/is/annotation_test.ts index 390214c..fe427ca 100644 --- a/is/annotation_test.ts +++ b/is/annotation_test.ts @@ -23,7 +23,8 @@ import { isSyncFunction, isUndefined, } from "./core.ts"; -import is, { isOptionalOf } from "./annotation.ts"; +import { isObjectOf, isTupleOf, isUniformTupleOf } from "./factory.ts"; +import is, { isOptionalOf, isReadonlyOf } from "./annotation.ts"; const examples = { string: ["", "Hello world"], @@ -152,6 +153,39 @@ Deno.test("isOptionalOf", async (t) => { }); }); +Deno.test("isReadonlyOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot( + t, + isReadonlyOf(isTupleOf([isString, isNumber, isBoolean])).name, + ); + await assertSnapshot( + t, + isReadonlyOf(isUniformTupleOf(3, isString)).name, + ); + await assertSnapshot( + t, + isReadonlyOf(isObjectOf({ a: isString, b: isNumber, c: isBoolean })).name, + ); + }); + await t.step("returns proper type predicate", () => { + const a: unknown = undefined; + if (isReadonlyOf(isTupleOf([isString, isNumber, isBoolean]))(a)) { + assertType>(true); + } + if (isReadonlyOf(isUniformTupleOf(3, isString))(a)) { + assertType>(true); + } + if ( + isReadonlyOf(isObjectOf({ a: isString, b: isNumber, c: isBoolean }))(a) + ) { + assertType< + Equal> + >(true); + } + }); +}); + Deno.test("is", async (t) => { const mod = await import("./annotation.ts"); const casesOfAliasAndIsFunction = Object.entries(mod) diff --git a/is/factory.ts b/is/factory.ts index 42012c7..b873169 100644 --- a/is/factory.ts +++ b/is/factory.ts @@ -1,6 +1,11 @@ import type { FlatType } from "../_typeutil.ts"; import type { Predicate, PredicateType } from "./type.ts"; -import { isOptional, type WithOptional } from "./annotation.ts"; +import { + isOptional, + isReadonlyOf, + type WithOptional, + type WithReadonly, +} from "./annotation.ts"; import { isAny, isArray, @@ -10,11 +15,14 @@ import { type Primitive, } from "./core.ts"; import { + type GetMetadata, getPredicateFactoryMetadata, setPredicateFactoryMetadata, type WithMetadata, } from "../metadata.ts"; +type IsReadonlyOfMetadata = GetMetadata>; + /** * Return a type predicate function that returns `true` if the type of `x` is `T[]`. * @@ -231,12 +239,17 @@ type IsTupleOfMetadata = { * const _: readonly [number, string, boolean] = a; * } * ``` + * + * @deprecated Use `isReadonlyOf(isTupleOf(...))` instead. */ export function isReadonlyTupleOf< T extends readonly [Predicate, ...Predicate[]], >( predTup: T, -): Predicate> & WithMetadata; +): + & Predicate>> + & WithReadonly + & WithMetadata; export function isReadonlyTupleOf< T extends readonly [Predicate, ...Predicate[]], E extends Predicate, @@ -244,8 +257,9 @@ export function isReadonlyTupleOf< predTup: T, predElse: E, ): - & Predicate, ...PredicateType]> - & WithMetadata; + & Predicate, ...PredicateType]>> + & WithReadonly + & WithMetadata; export function isReadonlyTupleOf< T extends readonly [Predicate, ...Predicate[]], E extends Predicate, @@ -254,36 +268,21 @@ export function isReadonlyTupleOf< predElse?: E, ): & Predicate< - ReadonlyTupleOf | readonly [...ReadonlyTupleOf, ...PredicateType] + | Readonly> + | Readonly<[...ReadonlyTupleOf, ...PredicateType]> > - & WithMetadata { + & WithReadonly + & WithMetadata { if (!predElse) { - return setPredicateFactoryMetadata( - isTupleOf(predTup) as Predicate>, - { name: "isReadonlyTupleOf", args: [predTup] }, - ); - } else { - return setPredicateFactoryMetadata( - isTupleOf(predTup, predElse) as unknown as Predicate< - readonly [...ReadonlyTupleOf, ...PredicateType] - >, - { name: "isReadonlyTupleOf", args: [predTup, predElse] }, - ); + return isReadonlyOf(isTupleOf(predTup)); } + return isReadonlyOf(isTupleOf(predTup, predElse)); } type ReadonlyTupleOf = { [P in keyof T]: T[P] extends Predicate ? U : never; }; -type IsReadonlyTupleOfMetadata = { - name: "isReadonlyTupleOf"; - args: [ - Parameters[0], - Parameters[1]?, - ]; -}; - /** * Return a type predicate function that returns `true` if the type of `x` is `UniformTupleOf`. * @@ -368,31 +367,19 @@ type IsUniformTupleOfMetadata = { * const _: readonly [number, number, number, number, number] = a; * } * ``` + * + * @deprecated Use `isReadonlyOf(isUniformTupleOf(...))` instead. */ export function isReadonlyUniformTupleOf( n: N, pred: Predicate = isAny, ): - & Predicate> - & WithMetadata { - return setPredicateFactoryMetadata( - isUniformTupleOf(n, pred) as Predicate>, - { name: "isReadonlyUniformTupleOf", args: [n, pred] }, - ); + & Predicate>> + & WithReadonly + & WithMetadata { + return isReadonlyOf(isUniformTupleOf(n, pred)); } -type ReadonlyUniformTupleOf< - T, - N extends number, - R extends readonly T[] = [], -> = R["length"] extends N ? R - : ReadonlyUniformTupleOf; - -type IsReadonlyUniformTupleOfMetadata = { - name: "isReadonlyUniformTupleOf"; - args: Parameters; -}; - /** * Return a type predicate function that returns `true` if the type of `x` is `Record`. *