Use when adding new error messages to React, or seeing "unknown error code" warnings.
npx skills add jon23d/skillz --skill "effective-typescript"
Install specific skill from multi-skill repository
# Description
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.
# SKILL.md
name: effective-typescript
description: 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.
Effective TypeScript
Overview
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.
When to use
- Writing new TypeScript code
- Converting JavaScript to TypeScript
- Designing interfaces, generics, or utility types
- Configuring tsconfig
- Handling data from APIs, external libraries, or user input
Rules β follow these without exception
1. Never use any
any silences the compiler and makes type safety a lie. There is always a better alternative.
- Use
unknownfor values of uncertain type β then narrow before use - Use
Record<string, unknown>for plain objects with unknown shape - Use generics (
<T>) for code that must work across types - Use
neverfor exhaustiveness checks
No exceptions:
- "It's contained/explicit" β any still propagates. Use unknown.
- "The library types are bad" β Use unknown and narrow, or add a type declaration file.
- "It's just internal code" β any in internal code causes the same runtime errors.
2. Never use unchecked type assertions (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.
- For API/network responses β validate with a schema library (Zod, Valibot, etc.) or write a type guard
- For
JSON.parseβ validate the result before asserting the type - For external library data β use
unknown+ type guard, notas T
Acceptable uses of as:
- Narrowing within a type guard you've already proven: (value as MyType).field after checking isMyType(value)
- DOM types that the compiler can't infer: document.getElementById('x') as HTMLInputElement
Rationalization to reject:
- "The caller owns the assertion" β the caller cannot verify the runtime shape either
- "It's pragmatic" β pragmatic means it defers the bug, not eliminates it
3. Validate all external data at the boundary
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'
);
}
4. Use discriminated unions + exhaustiveness checks
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)}`);
}
}
}
5. Generics over any for reusable code
When 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];
}
6. Use utility types β don't repeat type definitions
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>β stripnull | undefined
Don't re-declare types that can be derived. If User changes, derived types update automatically.
7. tsconfig β always use strict mode and path aliases
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"skipLibCheck": true,
"outDir": "dist",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"]
}
"strict": trueis not optional β enable it from day one, even for prototypesnoUncheckedIndexedAccessβ array index access returnsT | undefined, notTexactOptionalPropertyTypesβ distinguishes{ x?: string }from{ x: string | undefined }- "It's just a prototype" β bugs built into prototypes ship to production
baseUrl+pathsare required in every app. Relative imports like../../../hooks/useUserare 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.
8. Trust inference β don't annotate what the compiler already knows
TypeScript's inference is strong. Redundant annotations add noise and create maintenance burden when types change.
Don't annotate:
- Variables assigned from typed expressions: const user = userFactory.build() β not const user: User = ...
- Return types the body makes obvious: function add(a: number, b: number) { return a + b }
- Callback parameters: .map((item) => ...) β not .map((item: SomeType) => ...)
- Generic type parameters the compiler resolves: useState(0) β not useState<number>(0)
Do annotate:
- Exported function signatures β they're module boundaries and documentation
- When inference produces any or a wider type than intended
- Empty collections that need a specific type: const items: User[] = []
- Complex return types that aren't obvious from the function body
Rationalization to reject:
- "Explicit types are more readable" β redundant types are noise, not documentation. If the right-hand side says new Map<string, User>(), writing const users: Map<string, User> = repeats information.
- "It catches bugs earlier" β the compiler already caught it. You're just typing it twice.
Common anti-patterns and fixes
as anyto do optional chaining onunknownβ use a type guard or optional chaining onunknownafter narrowingparseJSON<T>withreturn JSON.parse(s) as Tβ validate with Zod or a type guardresponse.json() as SomeTypeβ useSomeSchema.parse(await response.json())cache.get(key) as Tβ document that callers must track what they stored; returnunknownand let callers narrow- Interfaces with
[key: string]: anyβ useRecord<string, unknown>or a proper discriminated union
Red flags β stop and reassess
- About to write
anyβ useunknowninstead - About to write
as SomeTypeon external data β validate first JSON.parseresult used directly β validate before useswitchon a union with nodefault: neverexhaustiveness check β add it"strict": falsein tsconfig β set it totrue- Thinking "this is too complex to type properly" β use generics
- Thinking "I'll add proper types later" β types added later miss the bugs types were meant to catch
- Writing a relative import that traverses more than one directory (
../../) β define or use a path alias instead
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.