typescript-best-practices
SKILL.md
TypeScript Best Practices
Type Safety
- Enable
strict: trueintsconfig.json— never disable strict checks in production code. - Prefer
unknownoverany. Ifanyis unavoidable, add a comment explaining why and narrow it immediately. - Use
satisfiesto validate a value matches a type while preserving its narrowed literal type:
const config = {
endpoint: "/api/users",
timeout: 3000,
} satisfies Config;
- Prefer type narrowing (type guards,
inoperator,instanceof) over type assertions (as).
Type Inference
- Let TypeScript infer when the type is obvious — don't annotate what the compiler already knows:
// Redundant
const name: string = "Graham";
// Let it infer
const name = "Graham";
- Always annotate function return types for exported/public functions — it catches accidental return type changes and improves IDE performance:
export function getUser(id: string): User | undefined {
return users.get(id);
}
- Annotate function parameters — they cannot be inferred from implementation.
Types vs Interfaces
- Use
typefor unions, intersections, mapped types, and utility types. - Use
interfacefor object shapes that may be extended or implemented. - Be consistent within a codebase — pick one default and stick with it.
Enums and Constants
- Prefer
as constobjects overenum:
const Status = {
Active: "active",
Inactive: "inactive",
} as const;
type Status = (typeof Status)[keyof typeof Status];
- This gives you type safety, tree-shaking, and no runtime enum overhead.
Null Handling
- Prefer explicit
| undefinedin types over optional properties when the distinction matters. - Use optional chaining (
?.) and nullish coalescing (??) over manual null checks. - Avoid non-null assertions (
!) — narrow the type instead.
Generics
- Name generic parameters descriptively when there are multiple:
TInput,TOutputinstead ofT,U. - Constrain generics with
extendsto communicate intent:
function merge<T extends Record<string, unknown>>(a: T, b: Partial<T>): T {
return { ...a, ...b };
}
- Avoid over-genericizing — if a function only ever handles one type, don't make it generic.
Utility Types
- Use built-in utility types (
Partial,Required,Pick,Omit,Record,Readonly) instead of reimplementing them. Readonly<T>for data that should not be mutated.PickandOmitto derive subsets from existing types rather than duplicating fields.
Error Handling
- Type errors explicitly — don't rely on
catch (e)beingany:
try {
await fetchData();
} catch (error) {
if (error instanceof ApiError) {
handleApiError(error);
}
throw error;
}
- Create typed error classes for domain-specific errors.
Module Organization
- One type/interface per concern — avoid monolithic
types.tsfiles. - Co-locate types with the code that uses them.
- Export types from barrel files only when they form part of the public API.
- Use
import type/export typefor type-only imports to enable proper tree-shaking.
Naming Conventions
- PascalCase for types, interfaces, enums, and classes.
- camelCase for variables, functions, and methods.
- UPPER_SNAKE_CASE for true constants (compile-time values).
- Don't prefix interfaces with
Ior types withT— it's not C#.
Weekly Installs
7
Repository
grahamcrackers/skillsFirst Seen
Feb 25, 2026
Security Audits
Installed on
github-copilot7
codex7
kimi-cli7
gemini-cli7
cursor7
opencode7