👷 Type-check the TS floor in CI
Add a `compat` job that type-checks the whole suite and a consumer fixture against the minimum supported TypeScript (5.9), reusing the `dist/` artifact `build` produced and gating `publish`. The compiler is resolved by npx, so it never enters `devDependencies` or the local `check`/`verify` loop. The fixture imports the package by name, resolving the emitted declarations through the `exports` map; `expectTypeOf` / `.not.toBeAny()` make it reject an `any`-typed declaration, which a bare compile would accept. Correct the README consumer floor from >= 5.0 to >= 5.9 (set by `type-fest`) and drop the `node10` resolution claim, which the exports-only entry never satisfied.
This commit is contained in:
1 parent
45df45df4b
commit
75807c4bd8
10 files changed
+218
-8
No files matched your search
@@ -0,0 +1,77 @@
|
||||
// Consumer smoke test for the minimum supported TypeScript (see README
|
||||
// § Requirements). It imports the package by name, so it resolves through the
|
||||
// `exports` map to the emitted `dist/*.d.ts` — including the relative `.ts`
|
||||
// specifiers they keep — rather than to the source. Run by `npm run test:compat`
|
||||
// and the CI `compat` job only; never by `check` / `verify`.
|
||||
//
|
||||
// Why the `expectTypeOf` assertions: a compile that merely succeeds is a weak
|
||||
// oracle. An `any`-typed declaration would compile, but `expect-type`'s exact
|
||||
// equality and `.not.toBeAny()` reject `any`, so the assertions prove the
|
||||
// emitted types are real. See development/ci.md § TypeScript compatibility.
|
||||
|
||||
import { expectTypeOf } from "expect-type";
|
||||
import {
|
||||
getPrimitiveUnionMatcher,
|
||||
getPrimitiveUnionMatcherW,
|
||||
getTaggedUnionMatcher,
|
||||
getTaggedUnionMatcherW,
|
||||
} from "tiny-pattern-ts";
|
||||
|
||||
type ResultCode = "ok" | "created";
|
||||
|
||||
const toStatus = getPrimitiveUnionMatcher<ResultCode>()({
|
||||
ok: () => "OK",
|
||||
created: () => "CREATED",
|
||||
});
|
||||
|
||||
expectTypeOf(toStatus).not.toBeAny();
|
||||
expectTypeOf(toStatus).toEqualTypeOf<(shape: ResultCode) => string>();
|
||||
|
||||
// The widening twin keeps each handler's own return type in the union.
|
||||
const toStatusW = getPrimitiveUnionMatcherW<ResultCode>()(
|
||||
{ ok: () => "OK" as const },
|
||||
(rest) => {
|
||||
expectTypeOf(rest).not.toBeAny();
|
||||
expectTypeOf(rest).toEqualTypeOf<"created">();
|
||||
return "CREATED" as const;
|
||||
},
|
||||
);
|
||||
|
||||
expectTypeOf(toStatusW).not.toBeAny();
|
||||
expectTypeOf(toStatusW).toEqualTypeOf<
|
||||
(shape: ResultCode) => "OK" | "CREATED"
|
||||
>();
|
||||
|
||||
type Contact =
|
||||
| { kind: "email"; address: string }
|
||||
| { kind: "phone"; number: string };
|
||||
|
||||
const format = getTaggedUnionMatcher<Contact>()("kind")({
|
||||
email: (e) => {
|
||||
expectTypeOf(e).not.toBeAny();
|
||||
expectTypeOf(e).toEqualTypeOf<{ kind: "email"; address: string }>();
|
||||
return e.address;
|
||||
},
|
||||
phone: (p) => {
|
||||
expectTypeOf(p).not.toBeAny();
|
||||
expectTypeOf(p).toEqualTypeOf<{ kind: "phone"; number: string }>();
|
||||
return p.number;
|
||||
},
|
||||
});
|
||||
|
||||
expectTypeOf(format).not.toBeAny();
|
||||
expectTypeOf(format).toEqualTypeOf<(shape: Contact) => string>();
|
||||
|
||||
const formatW = getTaggedUnionMatcherW<Contact>()("kind")(
|
||||
{ email: (e) => e.address },
|
||||
(rest) => {
|
||||
expectTypeOf(rest).not.toBeAny();
|
||||
expectTypeOf(rest).toEqualTypeOf<{ kind: "phone"; number: string }>();
|
||||
return rest.number;
|
||||
},
|
||||
);
|
||||
|
||||
expectTypeOf(formatW).not.toBeAny();
|
||||
expectTypeOf(formatW).toEqualTypeOf<(shape: Contact) => string>();
|
||||
|
||||
export { format, formatW, toStatus, toStatusW };
|
||||
Reference in new issue
Block a user