From 813eebe74490ee05c57657accaf0bd36ad94ca0d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Feb 2026 01:19:05 +0000 Subject: [PATCH] feat: add const type parameters for literal type inference from Set definitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TypeScript 5.0 introduced `const` type parameters which prevent type widening during inference. Adding `const T` to `typeChecker`, `isOptionalOf`, `isArrayOf`, `isDictionaryOf`, and `ensure` allows TypeScript to infer literal union types when a `Set` of literals is passed as a definition. Before this change: const isColor = typeChecker(new Set(["red", "green", "blue"])); // TypeChecker ← literal types lost After this change: const isColor = typeChecker(new Set(["red", "green", "blue"])); // TypeChecker<"red" | "green" | "blue"> ← literals preserved A new test file (src/constTypeParams.test.ts) contains both compile-time type assertions (verified by tsc) and runtime checks covering all four combinators. https://claude.ai/code/session_014UV4gEoodBgPAE8d4jcoJ8 --- src/constTypeParams.test.ts | 91 +++++++++++++++++++++++++++++++++++++ src/ensure.ts | 5 +- src/typeChecker.ts | 8 ++-- 3 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 src/constTypeParams.test.ts diff --git a/src/constTypeParams.test.ts b/src/constTypeParams.test.ts new file mode 100644 index 00000000..cfadc556 --- /dev/null +++ b/src/constTypeParams.test.ts @@ -0,0 +1,91 @@ +/** + * Tests for `const` type parameter inference. + * + * TypeScript 5.0 introduced `const` type parameters, which make the compiler + * infer the most specific (literal) type for a type argument instead of + * widening it. This file verifies that `typeChecker`, `isOptionalOf`, + * `isArrayOf`, and `isDictionaryOf` all leverage `const T` so that + * Set-based definitions produce literal union types rather than `string`. + */ +import * as assert from "node:assert"; +import { test } from "node:test"; +import { + isArrayOf, + isDictionaryOf, + isOptionalOf, + typeChecker, +} from "./typeChecker.ts"; +import type { TypeChecker } from "./types.ts"; + +// --------------------------------------------------------------------------- +// Compile-time assertions +// +// The explicit type annotations below act as type-level tests: TypeScript +// raises a compile error if the inferred type does not match the annotation. +// Without `const T`, `typeChecker(new Set(["a", "b"]))` would return +// `TypeChecker`, which is *not* assignable to +// `TypeChecker<"a" | "b">`, so the line would fail `tsc`. +// --------------------------------------------------------------------------- + +/** Verify typeChecker infers literal union from a Set of string literals. */ +const _isAB: TypeChecker<"a" | "b"> = typeChecker(new Set(["a", "b"])); +/** Verify isOptionalOf infers literal union from a Set of string literals. */ +const _isOptionalAB: TypeChecker<"a" | "b" | undefined> = isOptionalOf( + new Set(["a", "b"]), +); +/** Verify isArrayOf infers literal union from a Set of string literals. */ +const _isArrayAB: TypeChecker> = isArrayOf( + new Set(["a", "b"]), +); +/** Verify isDictionaryOf infers literal union from a Set of string literals. */ +const _isDictAB: TypeChecker> = isDictionaryOf( + new Set(["a", "b"]), +); + +// Suppress "declared but never used" warnings. +void _isAB; +void _isOptionalAB; +void _isArrayAB; +void _isDictAB; + +// --------------------------------------------------------------------------- +// Runtime tests +// --------------------------------------------------------------------------- + +test("typeChecker(Set) accepts members and rejects non-members", () => { + const isColor = typeChecker(new Set(["red", "green", "blue"])); + assert.equal(isColor("red"), true); + assert.equal(isColor("green"), true); + assert.equal(isColor("blue"), true); + assert.equal(isColor("yellow"), false); + assert.equal(isColor(42), false); + assert.equal(isColor(null), false); +}); + +test("isOptionalOf(Set) accepts members and undefined", () => { + const isOptionalColor = isOptionalOf(new Set(["red", "green", "blue"])); + assert.equal(isOptionalColor("red"), true); + assert.equal(isOptionalColor(undefined), true); + assert.equal(isOptionalColor("yellow"), false); +}); + +test("isArrayOf(Set) accepts arrays of members", () => { + const isColors = isArrayOf(new Set(["red", "green", "blue"])); + assert.equal(isColors(["red", "green"]), true); + assert.equal(isColors([]), true); + assert.equal(isColors(["red", "yellow"]), false); +}); + +test("isDictionaryOf(Set) accepts records of members", () => { + const isColorDictionary = isDictionaryOf(new Set(["red", "green", "blue"])); + assert.equal(isColorDictionary({ a: "red", b: "green" }), true); + assert.equal(isColorDictionary({}), true); + assert.equal(isColorDictionary({ a: "yellow" }), false); +}); + +test("typeChecker(Set) works with numeric literals", () => { + const isSmallInt: TypeChecker<1 | 2 | 3> = typeChecker(new Set([1, 2, 3])); + assert.equal(isSmallInt(1), true); + assert.equal(isSmallInt(3), true); + assert.equal(isSmallInt(4), false); +}); diff --git a/src/ensure.ts b/src/ensure.ts index e569c8aa..37e552cb 100644 --- a/src/ensure.ts +++ b/src/ensure.ts @@ -18,7 +18,10 @@ import type { TypeDefinition } from "./types.ts"; * @param definition definition of the type. * @returns input value as the defined type. */ -export const ensure = (input: unknown, definition: TypeDefinition): T => { +export const ensure = ( + input: unknown, + definition: TypeDefinition, +): T => { const error = typeChecker(definition).test(input); if (error) { throw error; diff --git a/src/typeChecker.ts b/src/typeChecker.ts index 2dd7aebd..6fb1d879 100644 --- a/src/typeChecker.ts +++ b/src/typeChecker.ts @@ -117,7 +117,7 @@ const factory = /** * Returns `TypeChecker` from `TypeDefinition`. */ -export const isOptionalOf: ( +export const isOptionalOf: ( definition: TypeDefinition, typeName?: string, ) => TypeChecker = factory( @@ -139,7 +139,7 @@ export const isOptionalOf: ( /** * Returns `TypeChecker>` from `TypeDefinition`. */ -export const isArrayOf: ( +export const isArrayOf: ( definition: TypeDefinition, typeName?: string, ) => TypeChecker> = factory( @@ -163,7 +163,7 @@ export const isArrayOf: ( /** * Returns `TypeChecker>` from `TypeDefinition`. */ -export const isDictionaryOf: ( +export const isDictionaryOf: ( definition: TypeDefinition, typeName?: string, ) => TypeChecker> = factory( @@ -224,7 +224,7 @@ export const isDictionaryOf: ( * @param typeName A type name. * @returns A type checker. */ -export const typeChecker: ( +export const typeChecker: ( definition: TypeDefinition, typeName?: string, ) => TypeChecker = factory((d: TypeDefinition, typeName: string) => {