frontier/skills/effective-typescript/SKILL.md
Use when writing, reviewing, or refactoring TypeScript code — especially when tempted to use `any`, type assertions, unvalidated casts, or when designing types, generics, utility types, or tsconfig settings.
npx skillsauth add jon23d/skillz effective-typescriptInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
TypeScript's value comes from the compiler catching bugs at build time. Every any, unchecked cast, or missing guard is a hole where runtime errors sneak through.
anyany silences the compiler and makes type safety a lie. There is always a better alternative.
unknown for values of uncertain type — then narrow before useRecord<string, unknown> for plain objects with unknown shape<T>) for code that must work across typesnever for exhaustiveness checksNo exceptions:
any still propagates. Use unknown.unknown and narrow, or add a type declaration file.any in internal code causes the same runtime errors.as T)as T is a promise to the compiler you cannot keep. If the data doesn't match T, you get silent undefined behavior.
JSON.parse → validate the result before asserting the typeunknown + type guard, not as TAcceptable uses of as:
(value as MyType).field after checking isMyType(value)document.getElementById('x') as HTMLInputElementRationalization to reject:
Data from APIs, JSON.parse, req.body, user input, and third-party libraries is unknown until proven otherwise.
Pattern:
// BAD
const user = await response.json() as User;
// GOOD — with Zod
import { z } from 'zod';
const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.string() });
const user = UserSchema.parse(await response.json()); // throws on mismatch
If you can't use a schema library, write an explicit type guard:
function isUser(val: unknown): val is User {
return (
typeof val === 'object' && val !== null &&
typeof (val as Record<string, unknown>).name === 'string'
);
}
Model variants with a literal type or kind field. Always add an exhaustiveness check.
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rect'; width: number; height: number };
function area(s: Shape): number {
switch (s.kind) {
case 'circle': return Math.PI * s.radius ** 2;
case 'rect': return s.width * s.height;
default: {
const _exhaustive: never = s; // compile error if a variant is unhandled
throw new Error(`Unhandled: ${JSON.stringify(_exhaustive)}`);
}
}
}
any for reusable codeWhen code must work across multiple types, use a type parameter — not any.
// BAD
function first(arr: any[]): any { return arr[0]; }
// GOOD
function first<T>(arr: T[]): T | undefined { return arr[0]; }
Constrain generics when the type must satisfy a shape:
function getField<T extends object, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
Partial<T> — all fields optionalRequired<T> — all fields requiredReadonly<T> — immutablePick<T, K> / Omit<T, K> — structural subsetsReturnType<typeof fn> — infer from functionParameters<typeof fn> — infer from function paramsNonNullable<T> — strip null | undefinedDon't re-declare types that can be derived. If User changes, derived types update automatically.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"skipLibCheck": true,
"outDir": "dist",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"]
}
"strict": true is not optional — enable it from day one, even for prototypesnoUncheckedIndexedAccess — array index access returns T | undefined, not TexactOptionalPropertyTypes — distinguishes { x?: string } from { x: string | undefined }baseUrl + paths are required in every app. Relative imports like ../../../hooks/useUser are hard to read and break silently when files move. Always import via the alias instead:// Bad — fragile and hard to read
import { useUser } from '../../../hooks/useUser'
import { Button } from '../../components/Button'
// Good — clear and refactor-safe
import { useUser } from '@/hooks/useUser'
import { Button } from '@/components/Button'
Bundlers don't read tsconfig.json automatically — pair the alias definition with the appropriate plugin (vite-tsconfig-paths for Vite, tsconfig-paths-webpack-plugin for Webpack). TypeScript and the bundler must always agree on what @/ resolves to.
TypeScript's inference is strong. Redundant annotations add noise and create maintenance burden when types change.
Don't annotate:
const user = userFactory.build() — not const user: User = ...function add(a: number, b: number) { return a + b }.map((item) => ...) — not .map((item: SomeType) => ...)useState(0) — not useState<number>(0)Do annotate:
any or a wider type than intendedconst items: User[] = []Rationalization to reject:
new Map<string, User>(), writing const users: Map<string, User> = repeats information.as any to do optional chaining on unknown → use a type guard or optional chaining on unknown after narrowingparseJSON<T> with return JSON.parse(s) as T → validate with Zod or a type guardresponse.json() as SomeType → use SomeSchema.parse(await response.json())cache.get(key) as T → document that callers must track what they stored; return unknown and let callers narrow[key: string]: any → use Record<string, unknown> or a proper discriminated unionany → use unknown insteadas SomeType on external data → validate firstJSON.parse result used directly → validate before useswitch on a union with no default: never exhaustiveness check → add it"strict": false in tsconfig → set it to true../../) → define or use a path alias insteaddevelopment
Use when adding or modifying environment variable handling in TypeScript projects or monorepos — especially when using process.env directly, missing startup validation, sharing env schemas across packages, or encountering "undefined is not a string" errors at runtime from missing env vars.
testing
Use when creating a new skill, editing an existing skill, writing a SKILL.md, or verifying a skill works before deployment.
development
React UI design principles and conventions. Load when building or modifying any user interface or React components. Covers application type detection, visual standards, component design and structure, Mantine (business apps) and Tailwind (consumer apps), accessibility, responsiveness, state management, data fetching, testing, and in-app help patterns.
development
Use when setting up ESLint and/or Prettier in a TypeScript project, adding linting to an existing TypeScript codebase, or configuring typescript-eslint, eslint-config-prettier, or related packages.