From typescript
TypeScript best practices for type safety and ergonomic code
How this skill is triggered — by the user, by Claude, or both
Slash command
/typescript:typescriptThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Write type-safe, ergonomic TypeScript. Prioritize correctness and developer experience.
Write type-safe, ergonomic TypeScript. Prioritize correctness and developer experience.
Always ensure these compiler options are enabled:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"strictNullChecks": true
}
}
anyEverything should be typed. If you need escape hatches:
// Bad
const data: any = fetchData();
// Better - use unknown and narrow
const data: unknown = fetchData();
if (isUser(data)) {
console.log(data.name);
}
// Or use generics
function fetchData<T>(): T { ... }
Reuse types from libraries and codebase. Use Pick/Omit for ergonomics:
// Bad - duplicating types
interface CreateUserInput {
name: string;
email: string;
}
// Good - derive from existing
type CreateUserInput = Pick<User, 'name' | 'email'>;
// Good - omit what you don't need
type UserWithoutId = Omit<User, 'id' | 'createdAt'>;
// Good - for function params
function updateUser(id: string, data: Partial<Pick<User, 'name' | 'email'>>) { ... }
// Preferred
interface User {
id: string;
name: string;
}
// Use type for unions, intersections, mapped types
type Status = 'pending' | 'active' | 'inactive';
type UserWithRole = User & { role: Role };
// Unnecessary - TypeScript infers this
const name: string = 'John';
const count: number = 0;
const users: User[] = [];
// Better - let inference work
const name = 'John';
const count = 0;
const users: User[] = []; // Keep when empty array needs type
// Bad - casting
const user = data as User;
// Better - use generics
const user = fetchData<User>();
// Better - type the variable
const user: User = { id: '1', name: 'John' };
// Better - use type guards
if (isUser(data)) {
// data is User here
}
When a type is complex and isolated, focus on input/output ergonomics:
// Input and output are well-typed, internal casting is acceptable
function transformData(input: RawData): ProcessedData {
const intermediate = input.items.map(item => {
// Complex transformation - casting ok here if isolated
return processItem(item) as ProcessedItem;
});
return { items: intermediate };
}
Use TypeScript's narrowing capabilities. Reference: https://www.typescriptlang.org/docs/handbook/2/narrowing.html
interface LoadingState {
status: 'loading';
}
interface SuccessState {
status: 'success';
data: User[];
}
interface ErrorState {
status: 'error';
error: Error;
}
type State = LoadingState | SuccessState | ErrorState;
function render(state: State) {
switch (state.status) {
case 'loading':
return <Spinner />;
case 'success':
return <UserList users={state.data} />;
case 'error':
return <Error message={state.error.message} />;
}
}
function handleError(error: unknown) {
if (error instanceof ValidationError) {
return { field: error.field, message: error.message };
}
if (error instanceof Error) {
return { message: error.message };
}
return { message: 'Unknown error' };
}
interface Dog { bark(): void; }
interface Cat { meow(): void; }
function speak(animal: Dog | Cat) {
if ('bark' in animal) {
animal.bark();
} else {
animal.meow();
}
}
Essential for filter functions:
// Type predicate
function isNonNull<T>(value: T | null | undefined): value is T {
return value != null;
}
// Usage with filter
const users: (User | null)[] = [...];
const validUsers: User[] = users.filter(isNonNull);
// Without predicate, TypeScript doesn't narrow
const broken = users.filter(u => u != null); // Still (User | null)[]
Don't wrap code in try/catch unless you're actually handling the error meaningfully:
// Bad - catching just to rethrow or log
try {
await saveUser(user);
} catch (error) {
console.error(error);
throw error;
}
// Bad - swallowing errors silently
try {
await saveUser(user);
} catch {
// silent failure
}
// Good - let errors propagate naturally
await saveUser(user);
// Good - actual error handling with recovery or transformation
try {
await saveUser(user);
} catch (error) {
if (error instanceof DuplicateEmailError) {
return { success: false, message: 'Email already exists' };
}
throw error; // rethrow unknown errors
}
// Good - cleanup with finally (but consider using `using` instead)
const connection = await getConnection();
try {
await connection.query(sql);
} finally {
await connection.release();
}
Only use try/catch when you:
using keyword when available)Prefer functional patterns:
// Bad - imperative loop
const results: string[] = [];
for (const user of users) {
if (user.active) {
results.push(user.name);
}
}
// Good - functional
const results = users
.filter(user => user.active)
.map(user => user.name);
// Good - with type predicate
const activeNames = users
.filter((user): user is ActiveUser => user.active)
.map(user => user.name);
A task is NOT complete until all type errors are resolved in modified files.
Before marking any task as done:
bun run typecheck (or tsc --noEmit)If the typecheck hook reports errors after your edits, you must fix them before proceeding to the next task.
| Do | Don't |
|---|---|
interface User | type User = { ... } (for objects) |
Pick<User, 'name'> | Duplicate type definitions |
const x = 5 | const x: number = 5 |
fetchData<User>() | fetchData() as User |
.filter(isNonNull) | .filter(x => x != null) without predicate |
.map().filter() | for loops for transformations |
npx claudepluginhub kingstinct/.github --plugin typescriptTypeScript type system, strict mode, and TS-specific patterns beyond JavaScript fundamentals. Invoke whenever task involves any interaction with TypeScript code — writing, reviewing, refactoring, debugging .ts/.tsx files, type definitions, generics, narrowing, tsconfig, or type-level programming.
Provides TypeScript best practices for type-safe code including strict mode, interfaces, discriminated unions, generics, async patterns, and null safety. Useful for type definitions and maintainable TS.
Enforces TypeScript conventions for strict, type-safe code: no `any` use unknown, interfaces vs types, literal unions over enums, discriminated unions, type narrowing with guards, branded types, and anti-pattern avoidance.