From eb6f8ed86847b5b4aa0bfba6b4bee201e8270619 Mon Sep 17 00:00:00 2001 From: Kei Ito Date: Thu, 19 Feb 2026 15:18:34 +0900 Subject: [PATCH 1/2] refactor(types): reduce unsafe casts and adopt variadic tuple args --- src/ensure.ts | 2 +- src/parseIpv4Address.ts | 20 ++++++++++++---- src/parseIpv6Address.ts | 30 ++++++++++++++++++++---- src/typeChecker.ts | 51 ++++++++++++++++++++++------------------- 4 files changed, 70 insertions(+), 33 deletions(-) diff --git a/src/ensure.ts b/src/ensure.ts index 37e552cb..e616452d 100644 --- a/src/ensure.ts +++ b/src/ensure.ts @@ -20,7 +20,7 @@ import type { TypeDefinition } from "./types.ts"; */ export const ensure = ( input: unknown, - definition: TypeDefinition, + definition: TypeDefinition>, ): T => { const error = typeChecker(definition).test(input); if (error) { diff --git a/src/parseIpv4Address.ts b/src/parseIpv4Address.ts index 915139da..178eccde 100644 --- a/src/parseIpv4Address.ts +++ b/src/parseIpv4Address.ts @@ -64,6 +64,18 @@ export interface Ipv4AddressParseResult { end: number; } +/** + * Argument tuple for `parseIpv4Address`. + */ +export type ParseIpv4AddressArgs = [ + input: string, + ...rest: [start: number] | [], +]; + +const isIpv4OctetTuple = ( + octets: Array, +): octets is Ipv4AddressParseResult["octets"] => octets.length === 4; + /** * Parses an IPv4 address. * @param input - The input string. @@ -71,14 +83,14 @@ export interface Ipv4AddressParseResult { * @returns The result of parsing an IPv4 address. */ export const parseIpv4Address = ( - input: string, - start = 0, + ...[input, start = 0]: ParseIpv4AddressArgs ): Ipv4AddressParseResult => { const octets: Array = []; for (const octet of listIpv4Octets(input, start)) { - if (octets.push(octet.value) === 4) { + octets.push(octet.value); + if (isIpv4OctetTuple(octets)) { return { - octets: octets as [number, number, number, number], + octets, start, end: octet.end, }; diff --git a/src/parseIpv6Address.ts b/src/parseIpv6Address.ts index 054c6c65..3d3d0387 100644 --- a/src/parseIpv6Address.ts +++ b/src/parseIpv6Address.ts @@ -115,6 +115,18 @@ export interface Ipv6AddressParseResult { end: number; } +/** + * Argument tuple for `parseIpv6Address`. + */ +export type ParseIpv6AddressArgs = [ + input: string, + ...rest: [start: number] | [], +]; + +const isIpv6AddressGroups = ( + groups: Array, +): groups is Ipv6AddressGroups => groups.length === 8; + /** * Parses an IPv6 address. * @param input - The input string. @@ -122,8 +134,7 @@ export interface Ipv6AddressParseResult { * @returns The result of parsing an IPv6 address. */ export const parseIpv6Address = ( - input: string, - start = 0, + ...[input, start = 0]: ParseIpv6AddressArgs ): Ipv6AddressParseResult => { const groups: Array = []; let compressorIndex = -1; @@ -146,15 +157,21 @@ export const parseIpv6Address = ( const length = groups.push(value); if (0 <= compressorIndex && length === 7) { groups.splice(compressorIndex, 0, 0); + if (!isIpv6AddressGroups(groups)) { + throw new Error(`InvalidIpv6Address: ${input.substr(start, end)}`); + } return { - groups: groups as Ipv6AddressGroups, + groups, start, end, }; } if (length === 8) { + if (!isIpv6AddressGroups(groups)) { + throw new Error(`InvalidIpv6Address: ${input.substr(start, end)}`); + } return { - groups: groups as Ipv6AddressGroups, + groups, start, end, }; @@ -169,8 +186,11 @@ export const parseIpv6Address = ( result.push(0); } result.push(...groups.slice(compressorIndex)); + if (!isIpv6AddressGroups(result)) { + throw new Error(`InvalidIpv6Address: ${input.substr(start, end)}`); + } return { - groups: result as Ipv6AddressGroups, + groups: result, start, end, }; diff --git a/src/typeChecker.ts b/src/typeChecker.ts index 6fb1d879..67307eb7 100644 --- a/src/typeChecker.ts +++ b/src/typeChecker.ts @@ -17,7 +17,7 @@ const is$String = (v: unknown): v is string => typeof v === "string"; const is$RegExp = (v: unknown): v is RegExp => getType(v) === "RegExp"; const is$Set = (v: unknown): v is Set => getType(v) === "Set"; const is$Function = (v: unknown): v is Callable => typeof v === "function"; -const is$Object = (v: unknown): v is Record => +const is$Object = (v: unknown): v is Record => is$Function(v) || (typeof v === "object" && v !== null); const lazy = (getter: () => T): (() => T) => { let cached: { value: T } | null = null; @@ -36,7 +36,7 @@ interface FactoryProps { test?( this: TypeChecker, input: unknown, - route?: Array, + route?: Array, ): Error | null; } @@ -90,11 +90,7 @@ const factory = if (cached) { return cached.value; } - if (is$Object(arg)) { - cache.set(arg, { value: typeGuard as TypeChecker }); - } - checkerCache.set(typeGuard, { value: typeGuard as TypeChecker }); - return defineProperties, TypeGuard>(typeGuard, { + const checker = defineProperties, TypeGuard>(typeGuard, { test: test ? { value: test } : { @@ -112,6 +108,11 @@ const factory = }, }, }); + if (is$Object(arg)) { + cache.set(arg, { value: checker }); + } + checkerCache.set(typeGuard, { value: checker }); + return checker; }; /** @@ -267,17 +268,26 @@ export const typeChecker: ( }, }; } - type PropertyTuple = [K, TypeChecker]; + type PropertyTuple = readonly [ + K, + TypeChecker, + ]; + const toPropertyTuple = (k: K): PropertyTuple => { + const propertyPath = `${typeName}.${String(k)}`; + return [k, typeChecker(d[k], propertyPath)]; + }; const properties = lazy( - (): Array => [ - ...(function* () { - for (const k of keys(d)) { - const propertyPath = `${typeName}.${String(k)}`; - yield [k, typeChecker(d[k], propertyPath)] as PropertyTuple; - } - })(), - ], + (): Array => keys(d).map((k) => toPropertyTuple(k)), ); + type TypeGuardWithRefs = ( + input: unknown, + refs?: WeakMap, + ) => input is U; + const runGuard = ( + checker: TypeChecker, + input: unknown, + refs: WeakMap, + ): boolean => (checker as TypeGuardWithRefs)(input, refs); const typeGuard = ( v: unknown, refs = new WeakMap(), @@ -289,12 +299,7 @@ export const typeChecker: ( throw new Error(`CircularReference: ${refs.get(v)} -> ${typeName}`); } refs.set(v, typeName); - return properties().every(([k, is]) => - (is as (input: T[typeof k], refs: WeakMap) => boolean)( - (v as T)[k], - refs, - ), - ); + return properties().every(([k, is]) => runGuard(is, v[k], refs)); }; return { typeGuard, @@ -320,7 +325,7 @@ export const typeChecker: ( return new TypeCheckError(this, input); } for (const [k, pd] of properties()) { - const error = pd.test((input as T)[k], route.concat(k)); + const error = pd.test(input[k], route.concat(k)); if (error) { return error; } From 1d46c5ec19ccb21bbf4cc3acd60083448cdc0bc2 Mon Sep 17 00:00:00 2001 From: Kei Ito Date: Thu, 19 Feb 2026 15:27:38 +0900 Subject: [PATCH 2/2] docs: fix comments --- src/ensure.ts | 10 +++++----- src/is/DomainName.ts | 2 +- src/is/Ipv4Address.ts | 2 +- src/is/Ipv6Address.ts | 2 +- src/typeChecker.ts | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/ensure.ts b/src/ensure.ts index e616452d..b3ff8795 100644 --- a/src/ensure.ts +++ b/src/ensure.ts @@ -2,8 +2,8 @@ import { typeChecker } from "./typeChecker.ts"; import type { TypeDefinition } from "./types.ts"; /** - * It ensures that the input matches the definition and returns the input. - * The returned value is guaranteed to be of the type defined by the definition. + * Ensures that the input matches the definition and returns it. + * The returned value is guaranteed to match the defined type. * @example * ```typescript * const isProduct = typeChecker({ @@ -14,9 +14,9 @@ import type { TypeDefinition } from "./types.ts"; * const response = await fetch("https://api.example.com/product/1"); * const product = ensure(await response.json(), isProduct); * ``` - * @param input value to be checked. - * @param definition definition of the type. - * @returns input value as the defined type. + * @param input The value to check. + * @param definition The type definition. + * @returns The input value as the defined type. */ export const ensure = ( input: unknown, diff --git a/src/is/DomainName.ts b/src/is/DomainName.ts index 61b2c488..df6f4ab6 100644 --- a/src/is/DomainName.ts +++ b/src/is/DomainName.ts @@ -30,7 +30,7 @@ export const isDomainName: TypeChecker = typeChecker( if (!isString(input)) { return false; } - /** Initial value is hyphen to return false for .example.com */ + /** Initialize with a hyphen so `.example.com` is rejected. */ let lastCodePoint = HYPHEN_MINUS; let currentLabelIsValid = false; let labelCount = 1; diff --git a/src/is/Ipv4Address.ts b/src/is/Ipv4Address.ts index c8cbfa97..e8ba2fcf 100644 --- a/src/is/Ipv4Address.ts +++ b/src/is/Ipv4Address.ts @@ -17,7 +17,7 @@ export const isIpv4Address: TypeChecker = typeChecker( const result = parseIpv4Address(input); return result.end === input.length; } catch { - // do nothing + // Ignore parse errors. } } return false; diff --git a/src/is/Ipv6Address.ts b/src/is/Ipv6Address.ts index 826a3d90..dcef3252 100644 --- a/src/is/Ipv6Address.ts +++ b/src/is/Ipv6Address.ts @@ -17,7 +17,7 @@ export const isIpv6Address: TypeChecker = typeChecker( const result = parseIpv6Address(input); return result.end === input.length; } catch { - // do nothing + // Ignore parse errors. } } return false; diff --git a/src/typeChecker.ts b/src/typeChecker.ts index 67307eb7..b99ab06a 100644 --- a/src/typeChecker.ts +++ b/src/typeChecker.ts @@ -57,9 +57,9 @@ const getCache = (context: object) => { export const typeCheckerConfig: { /** Clears the cache. */ clearCache(): void; - /** Gets the count of types without names. */ + /** Returns the number of unnamed types. */ getNoNameTypeCount(): number; - /** Resets the count of types without names. */ + /** Resets the unnamed type counter. */ resetNoNameTypeCount(count?: number): void; } = { clearCache() { @@ -195,7 +195,7 @@ export const isDictionaryOf: ( ); /** - * Create a type checker from a type definition. + * Creates a type checker from a type definition. * @example * ```typescript * // Create a type checker from a type guard function.