✅ Spec primitive union matchers

Type-driven tests: every expectTypeOf pairs an assert, covering the
header matrix (exhaustive vs partial patterns, strict R vs widened
returns, string and numeric keys, _ fallback wiring).

Negative cases use Parameters<typeof factory>[0] assignability because
expect-type's .not.toBeCallableWith collapses to never on these generic
Simplify<>-wrapped signatures.

Same documented expectTypeOf floating-promise false positive as
index.test.ts, so the same file-level disable applies; testing.md
known-issue updated to list both files.
This commit is contained in:
tmu committed 2026-09-16 14:52:30 +00:00
1 parent 40bf652b97
commit a25ac6d1e2
2 files changed
+130 -1

No files matched your search

+2 -1
View File
@@ -39,7 +39,8 @@ build step, and the runner relies on the `.ts` import-extension convention (see
## Known issues ## Known issues
- The type-aware linter misidentifies `expectTypeOf()` as a floating promise, so - The type-aware linter misidentifies `expectTypeOf()` as a floating promise, so
`src/index.test.ts` carries a file-level `oxlint-disable test files that use it (`src/index.test.ts`, `src/primitive.test.ts`) carry a
file-level `oxlint-disable
typescript/no-floating-promises` with an explanatory comment. It is a known typescript/no-floating-promises` with an explanatory comment. It is a known
false positive, not a rule worth disabling project-wide (see false positive, not a rule worth disabling project-wide (see
[tooling.md § oxlint-disable directives live next to the code](./tooling.md#oxlint-disable-directives-live-next-to-the-code)). [tooling.md § oxlint-disable directives live next to the code](./tooling.md#oxlint-disable-directives-live-next-to-the-code)).
+128
View File
@@ -0,0 +1,128 @@
/* oxlint-disable typescript/no-floating-promises -- expectTypeOf() is a sync type-assertion library that the type-aware linter misidentifies as a promise */
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { expectTypeOf } from "expect-type";
import {
getPrimitiveUnionMatcher,
getPrimitiveUnionMatcherPartial,
getPrimitiveUnionMatcherPartialW,
getPrimitiveUnionMatcherW,
} from "./primitive.ts";
// ============================================================================
// API: getPrimitiveUnionMatcherW — ✔️ Exhaustive / ❌ ReturnsStrict
// ============================================================================
test("getPrimitiveUnionMatcherW requires every literal key", () => {
const matcher = getPrimitiveUnionMatcherW<"a" | "b">()({
a: () => 1 as const,
b: () => "two" as const,
});
// ❌ ReturnsStrict: mixed handler returns widen to their union.
expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b") => 1 | "two">();
// A pattern missing a key must not satisfy the parameter type.
const factory = getPrimitiveUnionMatcherW<"a" | "b">();
expectTypeOf<{
a: () => number;
}>().not.toMatchTypeOf<Parameters<typeof factory>[0]>();
assert.equal(matcher("a"), 1);
assert.equal(matcher("b"), "two");
});
test("getPrimitiveUnionMatcherW dispatches on numeric literal keys", () => {
const matcher = getPrimitiveUnionMatcherW<1 | 2>()({
1: (n) => n + 1,
2: (n) => n * 10,
});
expectTypeOf(matcher).toEqualTypeOf<(shape: 1 | 2) => number>();
assert.equal(matcher(1), 2);
assert.equal(matcher(2), 20);
});
// ============================================================================
// API: getPrimitiveUnionMatcher — ✔️ Exhaustive / ✔️ ReturnsStrict
// ============================================================================
test("getPrimitiveUnionMatcher infers a single return type shared by all handlers", () => {
const matcher = getPrimitiveUnionMatcher<"a" | "b">()({
a: (): number => 1,
b: (): 1 | 2 => 2,
});
// ✔️ ReturnsStrict: R is the best common return type, not a widening union.
expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b") => number>();
assert.equal(matcher("a"), 1);
assert.equal(matcher("b"), 2);
});
test("getPrimitiveUnionMatcher handlers receive the matched literal", () => {
const matcher = getPrimitiveUnionMatcher<"on" | "off">()({
on: (s) => {
expectTypeOf(s).toEqualTypeOf<"on">();
return `handler ${s}`;
},
off: (s) => {
expectTypeOf(s).toEqualTypeOf<"off">();
return `handler ${s}`;
},
});
expectTypeOf(matcher).toEqualTypeOf<(shape: "on" | "off") => string>();
assert.equal(matcher("on"), "handler on");
assert.equal(matcher("off"), "handler off");
});
// ============================================================================
// API: getPrimitiveUnionMatcherPartial — ❌ Exhaustive / ✔️ ReturnsStrict
// ============================================================================
test("getPrimitiveUnionMatcherPartial routes shapes without a handler to _", () => {
const matcher = getPrimitiveUnionMatcherPartial<"a" | "b" | "c">()({
a: (): 1 | 2 => 1,
_: (s): 1 | 2 => {
// The fallback sees the whole union, not a single literal.
expectTypeOf(s).toEqualTypeOf<"a" | "b" | "c">();
return 2;
},
});
expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b" | "c") => 1 | 2>();
// ❌ Exhaustive: gaps are allowed, but only with a `_` fallback.
const factory = getPrimitiveUnionMatcherPartial<"a" | "b">();
expectTypeOf<{
a: () => number;
}>().not.toMatchTypeOf<Parameters<typeof factory>[0]>();
assert.equal(matcher("a"), 1);
assert.equal(matcher("b"), 2);
assert.equal(matcher("c"), 2);
});
test("getPrimitiveUnionMatcherPartial also accepts an exhaustive pattern", () => {
const matcher = getPrimitiveUnionMatcherPartial<"a" | "b">()({
a: () => "A",
b: () => "B",
});
expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b") => string>();
assert.equal(matcher("a"), "A");
assert.equal(matcher("b"), "B");
});
// ============================================================================
// API: getPrimitiveUnionMatcherPartialW — ❌ Exhaustive / ❌ ReturnsStrict
// ============================================================================
test("getPrimitiveUnionMatcherPartialW allows gaps and widens to the union of handler returns", () => {
const matcher = getPrimitiveUnionMatcherPartialW<"x" | "y" | "z">()({
x: () => 1 as const,
_: () => "fallback" as const,
});
expectTypeOf(matcher).toEqualTypeOf<
(shape: "x" | "y" | "z") => 1 | "fallback"
>();
const factory = getPrimitiveUnionMatcherPartialW<"x" | "y">();
expectTypeOf<{
x: () => number;
}>().not.toMatchTypeOf<Parameters<typeof factory>[0]>();
assert.equal(matcher("x"), 1);
assert.equal(matcher("y"), "fallback");
assert.equal(matcher("z"), "fallback");
});