| name | typescript-dev |
| description | TypeScript development guidelines and patterns. Use this skill when writing TypeScript code, creating React components with TypeScript, configuring tsconfig.json, using advanced types like generics or utility types, or when the user asks about TypeScript best practices. |
TypeScript Development Guidelines
Compiler Configuration
Strict Mode Settings
Always enable strict mode in tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true,
"noPropertyAccessFromIndexSignature": true
}
}
Path Aliases
Configure path aliases for clean imports:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"~types": ["src/types"],
"~components/*": ["src/components/*"],
"~features/*": ["src/features/*"]
}
}
}
Type Patterns
Prefer unknown Over any
function parse(input: any): object { ... }
function parse(input: unknown): object {
if (typeof input !== 'string') {
throw new Error('Expected string input');
}
return JSON.parse(input);
}
Use Type Guards Instead of Assertions
const user = data as User;
function isUser(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
'name' in data
);
}
if (isUser(data)) {
console.log(data.name);
}
Discriminated Unions for State
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function handleState<T>(state: AsyncState<T>) {
switch (state.status) {
case 'idle':
return null;
case 'loading':
return <Spinner />;
case 'success':
return <Data value={state.data} />;
case 'error':
return <ErrorMessage error={state.error} />;
}
}
Generic Constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
function sortItems<T extends { compareTo(other: T): number }>(items: T[]): T[] {
return [...items].sort((a, b) => a.compareTo(b));
}
Utility Types
| Type | Purpose | Example |
|---|
Partial<T> | All properties optional | Partial<User> for updates |
Required<T> | All properties required | Required<Config> for validation |
Pick<T, K> | Select specific properties | Pick<User, 'id' | 'name'> |
Omit<T, K> | Exclude specific properties | Omit<User, 'password'> |
Record<K, V> | Object with typed keys/values | Record<string, number> |
NonNullable<T> | Exclude null/undefined | NonNullable<string | null> |
Mapped Types
type Immutable<T> = {
readonly [K in keyof T]: T[K] extends object ? Immutable<T[K]> : T[K];
};
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type FirstParam<T> = T extends (first: infer P, ...args: any[]) => any ? P : never;
Conditional Types
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T;
type ExtractStrings<T> = T extends string ? T : never;
React + TypeScript Patterns
Component Definition
interface ButtonProps {
variant: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick: () => void;
children: React.ReactNode;
}
function Button({ variant, size = 'md', disabled, onClick, children }: ButtonProps) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
Refs (React 19+)
interface InputProps {
ref?: React.Ref<HTMLInputElement>;
label: string;
value: string;
onChange: (value: string) => void;
}
function Input({ ref, label, value, onChange }: InputProps) {
return (
<label>
{label}
<input ref={ref} value={value} onChange={(e) => onChange(e.target.value)} />
</label>
);
}
Hooks with Types
const [user, setUser] = useState<User | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = useCallback((data: FormData) => {
submitForm(data);
}, [submitForm]);
const sortedItems = useMemo(() =>
[...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
Event Handlers
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.clientX, e.clientY);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
submit();
}
};
Context with Types
interface ThemeContextValue {
theme: 'light' | 'dark';
toggle: () => void;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
Code Organization
Feature-Based Structure
src/
├── features/
│ └── auth/
│ ├── api/ # API calls
│ ├── components/ # Feature components
│ ├── hooks/ # Feature hooks
│ ├── types/ # Feature types
│ └── index.ts # Public exports
├── components/ # Shared components
├── hooks/ # Shared hooks
├── types/ # Global types
├── utils/ # Utility functions
└── lib/ # Third-party integrations
Barrel Exports
export { LoginForm } from './components/LoginForm';
export { useAuth } from './hooks/useAuth';
export type { User, AuthState } from './types';
Type-Only Imports
import type { User, AuthState } from './types';
import { validateUser } from './utils';
Validation with Zod
import { z } from 'zod';
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(['admin', 'user', 'guest']),
createdAt: z.coerce.date(),
});
type User = z.infer<typeof UserSchema>;
function parseUser(data: unknown): User {
return UserSchema.parse(data);
}
function tryParseUser(data: unknown): User | null {
const result = UserSchema.safeParse(data);
return result.success ? result.data : null;
}
Testing Patterns
Type-Safe Mocks
import { vi, type MockedFunction } from 'vitest';
const mockFetch = vi.fn() as MockedFunction<typeof fetch>;
function createUser(overrides: Partial<User> = {}): User {
return {
id: crypto.randomUUID(),
name: 'Test User',
email: 'test@example.com',
...overrides,
};
}
Testing React Components
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('submits form with valid data', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText('Email'), 'test@example.com');
await user.type(screen.getByLabelText('Password'), 'password123');
await user.click(screen.getByRole('button', { name: 'Login' }));
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
Common Pitfalls
Avoid Object Index Signatures Without Guards
const value = obj[key];
if (key in obj) {
const value = obj[key];
}
Handle Promise Rejections
async function fetchData() {
const response = await fetch('/api/data');
return response.json();
}
async function fetchData(): Promise<Result<Data, Error>> {
try {
const response = await fetch('/api/data');
if (!response.ok) {
return { success: false, error: new Error(`HTTP ${response.status}`) };
}
return { success: true, data: await response.json() };
} catch (error) {
return { success: false, error: error instanceof Error ? error : new Error(String(error)) };
}
}
Avoid Enums, Use Const Objects
enum Status {
Active = 'active',
Inactive = 'inactive',
}
const Status = {
Active: 'active',
Inactive: 'inactive',
} as const;
type Status = typeof Status[keyof typeof Status];
Type Narrowing in Callbacks
function process(value: string | null) {
if (value === null) return;
setTimeout(() => {
console.log(value.toUpperCase());
}, 100);
}
function process(value: string | null) {
if (value === null) return;
const safeValue = value;
setTimeout(() => {
console.log(safeValue.toUpperCase());
}, 100);
}
ESLint Configuration
import tseslint from 'typescript-eslint';
export default tseslint.config(
...tseslint.configs.strictTypeChecked,
{
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/strict-boolean-expressions': 'error',
},
}
);
See Also