typescript
Use when writing, testing or shipping TypeScript in the browser or Node — strict tsconfig, Vite builds, Canvas or Three.js games, deterministic logic with seeded randomness, i18n dictionaries, localSt
SKILL.mdskills/languages/typescript/SKILL.md
GitHubTypeScript
Default stack: TypeScript strict + Vite + Vitest + Playwright, ES modules, no runtime dependencies unless they clearly pay for themselves.
Project setup
npm create vite@latestwith thevanilla-ts(orreact-ts) template, then tightentsconfig.json:"strict": true,"noUncheckedIndexedAccess": true,"noImplicitOverride": true,"exactOptionalPropertyTypes": truewhen the codebase can take it."moduleResolution": "bundler"for Vite projects;"verbatimModuleSyntax": trueso type-only imports are explicit.
- Scripts in
package.json:dev,build(tsc --noEmit && vite build),test(vitest run),test:e2e(playwright test).vite buildalone does not type-check; keeptsc --noEmitin the build. - Pin the dev server port in a
playscript (vite --port <n> --strictPort) when other tools or people open the app by URL; a silently changed port breaks every link.
Structure that keeps logic testable
- Separate simulation (pure functions over plain data) from rendering (Canvas, WebGL, DOM). Unit-test the simulation without a browser; let Playwright cover only wiring.
- Route every random decision through one seeded generator (e.g. a small
mulberry32/xorshiftwrapper).Math.random()makes bugs unreproducible and benchmarks noisy. Lint for it. - Derive counts and labels from data at build or run time (number of levels, items, questions). Hard-coded numbers in UI text and test expectations drift silently.
- Put all user-visible strings in a dictionary per locale and look them up by key; test that every key exists in every locale.
Persistence
- Version the storage key (
app.save.v2) and write a migration from the previous version. Never delete or overwrite an older key in place: users roll back, and a failed migration should leave the old save readable. - Treat stored data as untrusted input: validate shape on load and fall back to defaults instead of throwing.
Verification loop
npx tsc --noEmit— zero errors.npx vitest run— unit tests, including seeded determinism tests (same seed → same result).npx playwright test— at least one smoke test per screen; assert the browser console has no errors or warnings.- Check bundle size after
vite build(du -sh dist) against a budget, and fail the build in CI if it is exceeded.
Pitfalls
- Headless WebGL frame rates are not real. Software rendering (SwiftShader) in CI or headless Chromium gives numbers that are an order of magnitude lower than a real GPU; treat them as relative, never as a pass/fail on absolute fps.
requestAnimationFrametiming: use the timestamp argument and a fixed-step accumulator for game logic; variabledtmakes physics and replays diverge.- Pointer input: use Pointer Events for mouse, touch and pen together; set
touch-action: noneon the canvas or touch scrolling steals gestures. noUncheckedIndexedAccessforces handlingundefinedfrom array access — annoying at first, but it catches the out-of-range bugs that strict mode alone misses.- Enums: prefer union types or
as constobjects;enumemits runtime code and interacts badly withisolatedModules. - Absolute asset paths (
/assets/x.png) break when the build is served from a sub-path; set Vitebaseor use relative paths. - Speech, audio and fonts load asynchronously:
speechSynthesis.getVoices()is empty at start-up,AudioContextmust be resumed after a user gesture, and web fonts may not be ready on the first frame. Wait for the ready signal rather than assuming.
When not to use it
For CPU-heavy simulation on the web, compare against C/C++ or Rust compiled to WebAssembly before optimising TypeScript further; for desktop-only titles, a native engine is often simpler.