Rewriting without any was evaluated; the alternatives were more
complex than the current solution. Record the rationale next to the
two oxlint-disable sites so the suppression is not re-litigated.
The primitive union matchers invoke every handler with the matched
literal, but the specs wrote their handlers with zero parameters, so
the passed value and its inferred key-literal type went untested.
Declare the parameter on each such handler and pin it with
expectTypeOf, matching the style the two already-correct handlers used.
The `_` fallbacks assert the whole union rather than a single literal.
Return-type expectations are unchanged, so the W / non-W widening
distinctions are still covered.
getPrimitiveUnionMatcherPartialW was declared with the intended
P-inferring signature but assigned via `as any`, hiding that the
declared parameter was not assignable to the implementation's. The
union in PatternPrimitiveUnionPartial makes the `_` arm demand
`_ ∈ keyof P`, which the exhaustive arm cannot prove.
Intersect the inference hook Simplify<P> with the implementation's
parameter shape, R pinned to PatternReturns<P>, so the assignment
type-checks while callers keep inferring P from the argument.
Add a spec for the exhaustive (no `_`) pattern.
Rework every test body into // Arrange / // Act / // Assert blocks
separated by blank lines: the factory is arranged once, the matcher is
built from it in a single act, and all type/runtime checks sink to the
end. index.test.ts adopts the same shape.
Also migrate off expect-type's deprecated toMatchTypeOf: the checks
are assignability tests, so toExtend is the faithful replacement.
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.
Curried matchers over string|number literal unions in four variants
(exhaustive/partial x strict/widened return inference), per the header
matrix in src/primitive.ts.
- Adds type-fest for Simplify/ValueOf.
- Deliberate oxlint escape hatches (as any dispatch) until a cast-free
formulation lands; see the file comments.
Cleaner separation of concerns:
- options.typeAware: true in .oxlintrc.json activates type-aware
rules declaratively (equivalent to --type-aware CLI flag, but
the script command stays clean: just 'oxlint ...')
- Remove the 3 type-aware rule disables from .oxlintrc.json
- Add source-level oxlint-disable directives instead:
- 4x typescript/no-unsafe-type-assertion (pattern.ts: keysMatch
Object.keys() cast, candidate[] cast; structuralMatcher value
as S cast; match.ts: handler as ... cast in nextCases)
- 1x typescript/no-unnecessary-type-parameters (pattern.ts:
keysMatch <S extends object>)
- 1x file-level typescript/no-floating-promises in index.test.ts
(expectTypeOf() is a sync type-assertion library that the
type-aware linter misidentifies)
Disabling rules at the source (next to the line that needs the
exemption) documents intent more clearly than a global config
override, and makes the trade-off visible to anyone reading the
code. Re-enabling a rule in the future only requires removing the
inline comment, not editing a central config.
Activates type-aware rules via oxlint --type-aware, backed by
oxlint-tsgolint (TypeScript-Go). Catches unsafe type assertions,
unnecessary type parameters, and other issues regular oxlint
cannot see.
- Add oxlint-tsgolint devDep + 6 platform-specific native bindings
as optionalDependencies (same pattern as oxlint)
- Add --type-aware flag to check:oxlint
- Add 6 @oxlint-tsgolint/* platforms to check:outdated ignore list
- Disable 3 type-aware rules in .oxlintrc.json with rationale:
- typescript/no-unsafe-type-assertion, typescript/no-unnecessary-type-parameters:
fire on legitimate generic type machinery in keysMatch/MatchBuilder
that needs type-system restructuring (deferred to a follow-up)
- typescript/no-floating-promises in test files: expectTypeOf() is
a sync type-assertion library that oxlint-tsgolint misidentifies
Code simplifications enabled by the new strict checks:
- src/match.ts: drop value as unknown casts (T is already assignable
to unknown) and the redundant run(value) as R cast
- src/index.test.ts: drop unnecessary 'X' as 'X | Y' assertions in
match<...>(...) calls (literals are already assignable to the union)
Documentation updates in project-specs.md and README.md. Add
'tsgolint' to cspell word list.
Node's --strip-types does not rewrite import specifiers the way bundlers
and Deno do, so 'import ... from "./match.js"' literally looks for
match.js (not match.ts) and fails with ERR_MODULE_NOT_FOUND at test
time. Fix by switching source imports to .ts extensions and letting
TypeScript's rewriteRelativeImportExtensions do the rewrite at build
emit time.
Changes:
- tsconfig.json: enable allowImportingTsExtensions (works because the
root config has noEmit: true)
- tsconfig.build.json: enable rewriteRelativeImportExtensions so the
emitted dist/*.js files keep './match.js' style imports (correct for
consumers), not './match.ts'
- src/index.ts: import './match.ts' and './pattern.ts'
- src/match.ts: import './pattern.ts'
- src/index.test.ts: import './index.ts'
Verified: npm run test runs tsc --noEmit (passes) and node --test
--strip-types src/ — all 6 tests pass. tsc -p tsconfig.build.json
emits dist/*.js with './match.js' imports as before; consumers see no
change.
- Install expect-type@1.4.0 (devDependency)
- New src/index.test.ts exercises the public API with both runtime
assertions (node:assert/strict inside node:test) and type-level
assertions (expectTypeOf().toEqualTypeOf, toMatchTypeOf, toHaveProperty)
- Tests are excluded from tsc -p tsconfig.build.json (no .d.ts pollution
in dist/)
- Type-level checks happen during tsc --noEmit (run by 'npm run check' and
the test script); expectTypeOf assertions have no runtime side effect,
so they pair naturally with node --test in the same test() blocks
- cspell: add EDITMSG to dictionary (used in commit-message-template path)
- oxlint test-file overrides: silence no-nodejs-modules (we use node:
builtins) and no-magic-numbers (literals in tests are fine)
Coverage:
- match() returns a builder with with/exhaustive/otherwise
- P.literal narrows to its literal type, P.type to its typeof target
- exhaustive() returns the union of handler return types
- otherwise() falls back when no case matches
- exhaustive() throws when no case matches
Introduce the core builder and pattern constructors:
- match(value): chain .with(pattern, handler) and terminate with
.exhaustive() (throws on no match) or .otherwise(handler).
- P.literal, P.type, P.when, P.any, P.shape for pattern construction.
- Each pattern is a Matcher<T> whose matches acts as a type guard, so
handler parameters are narrowed to the matched type.
The MatchBuilder accumulates handlers and returns a new builder per
.with() call (immutable chaining); finalization runs the cases in order
and returns the first match's handler result.
TypeScript configuration relaxed in .oxlintrc.json:
- Disable eslint/no-undefined (we use undefined as the no-match sentinel)
- Disable eslint/sort-keys (handler order matters; alphabetical would
be wrong)
- Disable eslint/id-length (T/R/U/V generics are standard TS convention)
- Disable import/no-named-export (false positive for library entry points)
Verified:
- tsc --noEmit passes with strict + noUncheckedIndexedAccess
- tsc -p tsconfig.build.json emits dist/*.js + .d.ts + sourcemaps
- node -e "import('./dist/index.js')" loads and exposes { match, P }
- Delete public/vite.svg, src/typescript.svg, src/style.css
- Empty public/ directory removed
- .npmignore: drop 'public/' entry (directory no longer exists), add
.oxlintrc.json and .oxfmtrc.json to ignore list
- cspell: add .oxfmtrc, .oxlintrc, .sortpackagerc to dictionary so
config-file name references aren't flagged
- lefthook: drop 'outdated' from pre-commit (kept in commands block for
explicit CI invocation); it shouldn't block commits for being behind on
upstream patch releases
- Use tsc 7 for build (vite no longer needed for a Node library)
- Use node --test with --experimental-strip-types for tests (vitest dropped)
- Use c8 for coverage instead of vitest's built-in coverage
- Add tsconfig.build.json separating typecheck from emit config
- Drop DOM lib, add strict noUncheckedIndexedAccess/noImplicitOverride
- Pin engines.node to >=22.6 (required for --experimental-strip-types)
- Update CI to Node 24 LTS (unflagged strip-types)
- Add @types/node for node:test / node:assert types