typescript-typecheck — SKILL.md
Variant: standard · When to use: the skill is invoked, runs to completion, returns a configured type-check gate (tsconfig + scripts + CI step), control passes back to the caller.
Overview
TypeScript's compiler (tsc) is the only tool that actually checks types. Bundlers and transpilers in a typical TS toolchain — Vite (esbuild in dev, Rollup in prod) and the SWC-based React plugin — transpile/strip type annotations without checking them. That means vite build (or any esbuild/SWC build) compiles successfully even when the code is full of type errors. This skill sets up the missing piece: a dedicated tsc --noEmit type-check gate that runs locally and in CI, backed by a genuinely strict tsconfig, plus TypeScript project references so cross-package types resolve in a monorepo. It is the type-system analog of a separate static-analysis gate — a linter (Biome/ESLint) finds style and bug patterns but has no type system and cannot do this job. Pinned to TypeScript 5.x. Verified flag list and worked examples live in references/.
When to activate
- ✅ Setting up TypeScript type-checking for a project for the first time (a
typecheckscript + CI step). - ✅ Writing or tightening a
tsconfig.jsonand wanting it genuinely strict (not juststrict: true). - ✅ Configuring
moduleResolution: "bundler"for a Vite/esbuild/bundler app and getting the companion flags right. - ✅ Setting up TypeScript project references (
composite,references,tsc -b) so an app package can consume a library/generated package's types in a monorepo. - ✅ Adding a CI type-check gate (e.g. GitHub Actions with pnpm) that fails the build on a type error the bundler would have ignored.
Do NOT activate when:
- The task is lint/format rules — that is a linter's job (e.g. the
biomeskill); a linter does not type-check. - The task is the actual production bundle/build output — that belongs to the bundler (e.g. the
viteskill); this skill only owns thetsccheck. - The task is a deep TypeScript type-system tutorial (advanced conditional/mapped types, generics gymnastics) — this skill is about the gate and its config, not type theory.
- The task is wiring the monorepo task runner's pipeline/caching — that belongs to the task-runner skill (e.g.
turborepo); this skill only contributes thetypechecktask it should run.
Workflow
Step 1: Establish why the gate is separate (and never skip it)
Internalize and, where relevant, state in the setup: no transpiler type-checks.
- Vite's own docs state it only transpiles
.tsfiles and does not type-check, assuming "type checking is taken care of by your IDE and build process" — explicitly recommending atsc --noEmitstep. esbuild "only performs transpilation without type information." - The SWC-based React plugin (
@vitejs/plugin-react-swc) is the same: SWC strips types without checking them. Choosing SWC over Babel/esbuild is a build-speed decision owned by the bundler config — it does not add or remove a type-check; thetscgate is still required.
Conclusion: a standalone tsc --noEmit (or tsc -b --noEmit with references) gate is mandatory, not optional, and must be distinct from vite build.
Step 2: Write a genuinely strict tsconfig
strict: true is the floor, not the ceiling. It turns on a known family (full list + worked configs in references/strict-tsconfig.md):
noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables, alwaysStrict.
Add the high-value flags not covered by strict:
noUncheckedIndexedAccess— addsundefinedto index/array access results (the single biggest real-bug catcher beyondstrict).noImplicitOverride— requires theoverridekeyword so subclasses can't silently drift.noFallthroughCasesInSwitch,noImplicitReturns— control-flow completeness.noUnusedLocals,noUnusedParameters— dead-code (params prefixed_are exempt).exactOptionalPropertyTypes— distinguishes "absent" from "set toundefined" (see Gotchas — it is the one to add knowingly).verbatimModuleSyntax— drops imports/exports exactly per thetypemodifier you wrote (no elision guessing).
For a Vite/bundler app, the module block is: "module": "esnext" (or "preserve"), "moduleResolution": "bundler", "noEmit": true, "jsx": "react-jsx", "isolatedModules": true, "moduleDetection": "force". Easiest strict floor: "extends": "@tsconfig/strictest" then layer the bundler block on top.
Step 3: Adopt the Vite split-config layout (if Vite)
A Vite TS app scaffolds three files: a solution tsconfig.json that only holds "files": [] + "references" to the two leaf configs; tsconfig.app.json (your src/, DOM lib, bundler resolution, noEmit); tsconfig.node.json (vite.config.ts, node lib). package.json gets "build": "tsc -b && vite build" and you add a standalone "typecheck": "tsc -b --noEmit". The tsc -b step is type-check-only here because the leaf configs set noEmit. Layout + exact files in references/vite-split-config.md.
Step 4: Configure project references for the monorepo
For a multi-package repo where an app consumes a library or generated package (e.g. a generated API client): set "composite": true on the referenced package (this forces declaration: true and requires every source file be matched by include/files), add a "references": [{ "path": "../<pkg>" }] entry on the consumer, and build/check with tsc -b. Importing from a referenced project resolves against its emitted .d.ts, and tsc -b builds dependencies in order and is incremental via .tsbuildinfo. Worked 2-package example in references/project-references.md. Wire the resulting typecheck task into the monorepo task runner (caching, dependency order) via the turborepo skill — do not re-derive a pipeline here.
Step 5: Add the CI gate
Add a CI job that runs the typecheck script on every push/PR so a type error fails the build (the bundler never would). High-level GitHub Actions shape (checkout → pnpm + Node with cache → install → pnpm run typecheck or turbo run typecheck) in references/ci-gate.md; the pipeline/caching specifics belong to the task-runner skill.
Rules
Hard rules (never violate):
- The
tsccheck is separate from the build. Never claim or imply thatvite build/ esbuild / SWC validates types. Keeptypechecka distinct script and a distinct CI step. strict: trueis mandatory;strictalone is not "strict enough." Ship at leastnoUncheckedIndexedAccesson top, and prefer a strictest base.- Don't trust a scaffold to be strict — verify it. A freshly scaffolded
tsconfig.app.jsonmay not containstrict: true; open it and confirm (orextends: "@tsconfig/strictest"). Never assume. - With project references, check via
tsc -b, not baretsc. Baretsc -pon the solution file ignores thereferencesgraph;tsc -b(build mode) honors it and is incremental. composite: trueforcesdeclaration: trueand requires all files be ininclude/files— setoutDir/tsBuildInfoFiledeliberately.
Preferences (override-able):
- Prefer
extends: "@tsconfig/strictest"as the strict floor, then override the module/jsx block per project. - Prefer
module: "esnext"(or"preserve") withmoduleResolution: "bundler"for bundler apps;nodenextfor Node libraries. - Use
vite-plugin-checkerfor fast in-editor/in-dev feedback in addition to — never instead of — the CItscgate. - Keep
skipLibCheck: trueon by default for speed, knowing the trade-off (Gotchas).
Gotchas
- "
vite buildpassed, so the types are fine." False — the highest-value mistake this skill prevents. The bundler stripped the types; onlytsc --noEmitchecked them. Always run thetypecheckscript independently. - Scaffold is not automatically strict. The current create-vite
react-tstsconfig.app.jsoncarries the bundler + lint flags but you must confirmstrict: trueis actually present (it has drifted across template versions); add it orextends: "@tsconfig/strictest". moduleResolution: "bundler"has hard companions. It requiresmoduleto beesnextorpreserve(notcommonjs).allowImportingTsExtensions: trueis only legal whennoEmit(oremitDeclarationOnly) is set — which is exactly the type-check-only setup, so it fits, but emitting JS with it will error.exactOptionalPropertyTypessurprises. With it on,{ a?: string }no longer accepts{ a: undefined }— you must writea?: string | undefinedto allow that. It is not instrictfor this reason; add it deliberately and expect to touch optional-property call sites.- Forgetting
tsc -bin a references setup. Runningtsc --noEmitagainst a solutiontsconfig.jsonwith onlyreferenceschecks nothing useful — build mode (tsc -b) is what walks the graph. Thetypecheckscript must use-b. skipLibCheck: truehides real errors. It skips type-checking all.d.ts(including your generated client's emitted declarations and conflictingnode_moduleslib types). It's a deliberate speed/noise trade-off; if a generated.d.tsis itself malformed, this mask will let it through.verbatimModuleSyntaxneeds explicitimport type. With it on, a value-position import of a type is no longer elided and can crash at runtime — mark type-only imports withimport type. It supersedes the deprecatedimportsNotUsedAsValues/preserveValueImports.
Anti-patterns
- "We have a linter, so we don't need
tsc." A linter (Biome/ESLint) has no type system; it cannot catch a type error. The type-check gate is non-negotiable and orthogonal to lint. - "Switching to the SWC plugin removed the need for the
tscstep." No — SWC is a faster transpiler that still does not type-check. The build plugin choice is independent of the type-check gate. - "Just turn on
skipLibCheckand the errors go away." That suppresses declaration-file errors, not your code's errors, and can mask a broken generated.d.ts. Don't reach for it to silence a real type failure. - "Set
strict: trueand call the config strict." Strict-floor only; ship the extra flags (noUncheckedIndexedAccessat minimum) or a strictest base. - "Let CI rely on
vite buildto catch type problems." It never will. The CI gate must invoketsc/tsc -b --noEmit(directly or via the task runner) as its own step.
Output
A working TypeScript type-check gate for a project: a genuinely strict tsconfig set (solution + leaf configs where applicable), a standalone typecheck npm script (tsc --noEmit or tsc -b --noEmit) distinct from the build, project references wired for any consumed/generated package, and a CI step that runs the check and fails on type errors. The consumer is the next workflow phase — the developer or dispatched agent who now has a type-error gate the bundler does not provide, and CI that enforces it.
Related
biome(or any linter skill) — the lint/format complement; it does not type-check, which is why this skill exists.vite— owns the build/bundle; this skill owns only thetsccheck that the build skips.turborepo— owns the monorepo task pipeline/caching; this skill contributes thetypechecktask it runs.- Python analog (conceptual only, different toolchain): a standalone Python type-checker such as
typlays the same "separate type-check gate" role for Python thattsc --noEmitplays here.
Progressive disclosure
references/strict-tsconfig.md— the fullstrictfamily, the recommended extra flags with exact behavior, the bundler module block, shareable bases, and a worked stricttsconfig.app.json. Load when writing/tightening a tsconfig.references/vite-split-config.md— the three-file Vite TS layout, thetsc -b && vite build+typecheckscripts, andvite-plugin-checker. Load when setting up a Vite app's type-check.references/project-references.md— composite/references/tsc -b/incremental semantics with a worked 2-package (app → generated-client) example, andskipLibCheck. Load when wiring a monorepo.references/ci-gate.md— thetscCLI flag reference, the GitHub Actions gate shape, and the verifiedtsgo/typescript-go status note. Load when adding CI or asked about the native compiler.references/sources.md— research provenance.
Body budget
description≤ 1,024 chars.- Body ≤ ~500 lines / 5,000 tokens; heavy content lives in
references/, loaded on demand.