TypeScript language patterns, testing methodology, and build tooling for production applications. Covers: type safety, testing (Vitest/Playwright), performance optimization, security practices, linting, module patterns, and build configuration. Use when: configuring TypeScript strictness, writing tests, optimizing bundles, reviewing security, or setting up build tooling.
For React-specific patterns (hooks, state, data fetching, error boundaries), see the React skill (/react). For component design, form UX, and accessibility, see the UX Design skill (/ux-design). For CSS and responsive patterns, see the CSS & Responsive skill (/css-responsive).
tsconfig.json{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noPropertyAccessFromIndexSignature": true,
"noFallthroughCasesInSwitch": true,
"verbatimModuleSyntax": true
}
}
noUncheckedIndexedAccess — adds | undefined to array/object index access. Forces narrowing on arr[0] and lookups.RecordexactOptionalPropertyTypes — distinguishes "property missing" from "property is undefined". Optional foo?: string can be omitted but cannot be explicitly set to undefined.noPropertyAccessFromIndexSignature — forces bracket notation for index signatures, clarifying dynamic vs known property access.verbatimModuleSyntax — preserves ES module syntax untouched, critical for tree-shaking in Vite/Rollup.satisfies over type annotationsUse satisfies when you want validation against a type but need to preserve literal inference:
// WRONG: loses literal type information
const config: Config = { theme: "dark", retries: 3 };
// CORRECT: validates against Config but preserves literals
const config = { theme: "dark", retries: 3 } as const satisfies Config;
// config.theme is "dark", not string
For identifying which concepts need branded types (entities vs value objects, aggregate boundaries), see the Domain Design skill (
/domain-design§3-4).
Prevent mixing semantically different values that share the same primitive type:
type UserId = string & { readonly __brand: unique symbol };
type OrderId = string & { readonly __brand: unique symbol };
function createUserId(id: string): UserId { return id as UserId; }
function createOrderId(id: string): OrderId { return id as OrderId; }
function getUser(id: UserId): User { /* ... */ }
getUser(orderId); // compile error
Eliminate impossible states:
type FetchState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
No data on error, no error on success. Enables exhaustive switch narrowing.
any — use unknown and narrow, or use genericsas for type assertions unless you've proven correctness (prefer type guards)// @ts-ignore — use // @ts-expect-error with an explanation if truly neededenum — use as const objects or string literal unions (enums have runtime footprint and tree-shaking issues)// WRONG
enum Status { Active, Inactive }
// CORRECT
const Status = { Active: "active", Inactive: "inactive" } as const;
type Status = (typeof Status)[keyof typeof Status];
userEvent.Universal principles for deciding which layer a test belongs in:
// WRONG: testing implementation details
expect(component.state.isOpen).toBe(true);
// CORRECT: testing what the user sees
await userEvent.click(screen.getByRole("button", { name: /open menu/i }));
expect(screen.getByRole("menu")).toBeVisible();
userEvent over fireEventuserEvent simulates real user interactions (focus, hover, type individual characters). fireEvent dispatches raw DOM events that skip browser behavior:
// WRONG
fireEvent.change(input, { target: { value: "hello" } });
// CORRECT
await userEvent.type(input, "hello");
Mock at the network layer, not at the module layer. Reuse handlers across Vitest and Playwright:
import { http, HttpResponse } from "msw";
export const handlers = [
http.get("/api/resources", () => {
return HttpResponse.json([{ id: "1", name: "Test" }]);
}),
];
Always open every test with expect.assertions(N) where N is the total number of expect() calls in the test body. This prevents tests from silently passing when assertions are inside conditionals that never execute.
// WRONG — passes vacuously if body.intent is never "field_errors"
it("returns field errors on bad input", async () => {
const { body } = await callAction({ email: "bad" });
if (body.intent === "field_errors") {
expect(body.errors.email).toBeTruthy();
}
});
// CORRECT — fails if the expect() inside the if never ran
it("returns field errors on bad input", async () => {
expect.assertions(2);
const { body } = await callAction({ email: "bad" });
expect(body.intent).toBe("field_errors");
if (body.intent === "field_errors") {
expect(body.errors.email).toBeTruthy();
}
});
Count every expect() call, including those inside if branches, .forEach, and try/catch blocks.
Snapshots create noise, break on any DOM change, and discourage TDD:
// WRONG
expect(container).toMatchSnapshot();
// CORRECT: explicit assertions on what matters
expect(screen.getByRole("heading")).toHaveTextContent("Dashboard");
expect(screen.getByTestId("item-count")).toHaveTextContent("42");
page.getByRole("button", { name: "Submit" })page.waitForResponse for API-dependent assertionsReact Compiler (stable in React 19) automatically memoizes at build time:
useMemo, useCallback, and React.memo from new code// vite.config.ts
import reactCompiler from "babel-plugin-react-compiler";
The minimum viable optimization — 40-60% initial bundle reduction:
const ResourceDetail = React.lazy(() => import("./routes/resource-detail"));
Mandatory for production applications:
// vite.config.ts
import { visualizer } from "rollup-plugin-visualizer";