Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions src/constTypeParams.test.ts
Original file line number Diff line number Diff line change
@@ -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<string>`, 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<Array<"a" | "b">> = isArrayOf(
new Set(["a", "b"]),
);
/** Verify isDictionaryOf infers literal union from a Set of string literals. */
const _isDictAB: TypeChecker<Record<string, "a" | "b">> = 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);
});
5 changes: 4 additions & 1 deletion src/ensure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <T>(input: unknown, definition: TypeDefinition<T>): T => {
export const ensure = <const T>(
input: unknown,
definition: TypeDefinition<T>,
): T => {
const error = typeChecker(definition).test(input);
if (error) {
throw error;
Expand Down
8 changes: 4 additions & 4 deletions src/typeChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ const factory =
/**
* Returns `TypeChecker<T | undefined>` from `TypeDefinition<T>`.
*/
export const isOptionalOf: <T>(
export const isOptionalOf: <const T>(
definition: TypeDefinition<T>,
typeName?: string,
) => TypeChecker<T | undefined> = factory(
Expand All @@ -139,7 +139,7 @@ export const isOptionalOf: <T>(
/**
* Returns `TypeChecker<Array<T>>` from `TypeDefinition<T>`.
*/
export const isArrayOf: <T>(
export const isArrayOf: <const T>(
definition: TypeDefinition<T>,
typeName?: string,
) => TypeChecker<Array<T>> = factory(
Expand All @@ -163,7 +163,7 @@ export const isArrayOf: <T>(
/**
* Returns `TypeChecker<Record<string, T>>` from `TypeDefinition<T>`.
*/
export const isDictionaryOf: <T>(
export const isDictionaryOf: <const T>(
definition: TypeDefinition<T>,
typeName?: string,
) => TypeChecker<Record<string, T>> = factory(
Expand Down Expand Up @@ -224,7 +224,7 @@ export const isDictionaryOf: <T>(
* @param typeName A type name.
* @returns A type checker.
*/
export const typeChecker: <T>(
export const typeChecker: <const T>(
definition: TypeDefinition<T>,
typeName?: string,
) => TypeChecker<T> = factory(<T>(d: TypeDefinition<T>, typeName: string) => {
Expand Down