When a fallback argument is present, the second overload supplies the
contextual type, so the handler map popup offers optional keys (`a? b? c?`)
and handled keys stay optional (`b? c?`). The exhaustive one-argument popup
is unchanged. Covered for both factories.
The factory-contract test now also covers a fallback whose return does not
fit the common return of the handler map — a `number` handler with a
`string` fallback — which `getMatcherW` accepts as `number | string`.
The fallback overload could not infer `R` from the handlers: `R` appeared
only inside the `Exact` constraint, which is not an inference site, so with
inferred handler params it collapsed to `unknown` and the matcher silently
returned `UnaryFn<T, unknown>`. Adding `Partial<Handlers<T, R>>` to the
handler parameter gives `R` an inference site, so the common return is
inferred from the handlers and the fallback alike.
Tests drop their explicit handler return annotations: real handlers are short
and unannotated, and the common type is now inferred from them.
The fallback is now the second factory argument — `(handlers, (s) => …)` —
so its parameter is `Exclude<T, keyof handlers>`. A property's contextual
type is fixed before TypeScript infers its sibling keys, so the remainder is
not expressible while the fallback sits in the handler map; a later argument
is contextually typed from inference on an earlier one.
Both factories take the new shape; the tests and autocomplete probes follow.
The generic overloads guard excess keys with type-fest's `Exact`, so the
error lands on the offending property. A redundant fallback (a full handler
map plus one) is still accepted — the guard for it does not survive inference
and is left to its backlog task.
The protocol cross-link and the TS-internal shutdown-race explanation were
too detailed for the category doc. The helper comment keeps a short,
self-contained note on the stdin-EOF workaround.
The completion helper is test-only (excluded from dist/), so the
style-only no-magic-numbers and unicorn/no-null warnings are tolerable
here. A file-level disable keeps them out of check:oxlint without touching
the project config.
The TS 7 Go server races Session.updateWatches against the exit
notification; handleExit returns io.EOF, the background context is
cancelled, and the bare error is flushed to stderr once the outgoing
queue closes (server.go / logger.go). Send shutdown and close stdin
instead: EOF makes the server exit cleanly (code 0, no output), with the
kill kept as a fallback. testing.md records the cause.
Replace the hand-rolled JSON-RPC client (framing, pending map, reply
dispatch) with createMessageConnection and typed requests from
vscode-languageserver-protocol. A catch-all onRequest answers the server
requests the old client replied to, and CompletionList/CompletionItem
replace the ad-hoc shape guards. Public LspSession API is unchanged.
Drop the strict/widened aliases from src/index.ts so the factories are
exported under their own names, and align the type tests, the autocomplete
probe (which now imports the public surface from ./index.ts) and library.md
with them.
Also correct the widened-overload comment: the excess-property check does not
apply to a generic P, so MatcherWidening's keyof guard — not the closed
constraint — is what rejects keys outside T/_, as library.md already
documented.
The helper's own contract — marker handling, requested position, label
extraction and the rejection when the marker is absent — is asserted against
in-memory documents whose contextual types are written inline, so the helper
is tested without coupling to the library's code.
strict/widened each take the exhaustive/fallback pattern shape in three
overloads ordered ExhaustiveLoose -> Fallback -> Handlers. TypeScript reads
the first overload for the object-literal popup (_?, a, b) and the last for
the missing-key error, so autocomplete and the error message are tuned
independently. The fallback is a pattern shape, not a factory, so the four
getPrimitiveUnionMatcher* factories collapse to two.
src/index.ts re-exports the two factories; the prototype scratch files that
explored the alternatives are removed.
src/util/__tests__/ holds shared test helpers, reached from anywhere
in the source tree as `#test-utils/<name>.ts` through
package.json#imports. A bare `~/…` is not a valid imports key — Node
requires `#` — and a direction-free specifier keeps
import/no-relative-parent-imports from ever firing. The `__tests__`
folder name is the pattern tsconfig.build.json already excludes, so
helpers can never ship. Lint scoping: import/no-nodejs-modules off for
the folder (the probe spawns a language server) and
typescript/promise-function-async off to match its promise-returning
style. The LSP probe moves in from scripts/; a smoke test pins the
specifier's resolution and typing without starting a server.
They were the template's demo API, not the library's real surface. Drop
them, their README walkthrough, and the tooling note that cited their
disable directives, and point index.ts at the primitive matchers that
remain.
The spec only exercised the template's match/P example API. Removing it
first lets the modules and exports it imports go next without leaving a
dangling reference.
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