From typescript
TypeScript best practices for type safety and ergonomic code
npx claudepluginhub kingstinct/.github --plugin typescriptThis skill uses the workspace's default tool permissions.
Write type-safe, ergonomic TypeScript. Prioritize correctness and developer experience.
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 type safety rules including strict mode, no 'any', discriminated unions, and 'unknown' with type guards. Use when writing, reviewing types, or refactoring TS code.
Provides expert TypeScript guidance for type-safe apps, advanced types, strict mode, JS-to-TS refactoring, tsconfig optimization, and TDD workflows.
Share bugs, ideas, or general feedback.
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 |