npx claudepluginhub jsamuelsen11/claude-config --plugin ccfg-typescriptWant just this skill?
Then install: npx claudepluginhub u/[userId]/[slug]
This skill should be used when working on TypeScript projects, writing TypeScript code, configuring tsconfig, or reviewing TypeScript code.
This skill uses the workspace's default tool permissions.
TypeScript Conventions and Best Practices
This skill defines strict TypeScript conventions for maximum type safety, maintainability, and developer experience. These practices ensure production-ready code that leverages TypeScript's full type system capabilities.
Type Safety Principles
Strict Mode Required
ALWAYS enable strict mode and all strict options in tsconfig.json.
CORRECT:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedSideEffectImports": true,
"allowUnusedLabels": false,
"allowUnreachableCode": false
}
}
WRONG:
{
"compilerOptions": {
"strict": false,
"noImplicitAny": false
}
}
Never Use any
ALWAYS use unknown instead of any, then narrow with type guards.
WRONG:
function processData(data: any) {
return data.value; // No type safety
}
function fetchApi(): Promise<any> {
return fetch('/api/data').then((r) => r.json());
}
CORRECT:
function processData(data: unknown) {
if (typeof data === 'object' && data !== null && 'value' in data) {
return (data as { value: unknown }).value;
}
throw new Error('Invalid data shape');
}
// Better: Use type guard
type DataWithValue = { value: string };
function isDataWithValue(data: unknown): data is DataWithValue {
return (
typeof data === 'object' &&
data !== null &&
'value' in data &&
typeof (data as DataWithValue).value === 'string'
);
}
function processData(data: unknown) {
if (isDataWithValue(data)) {
return data.value; // Type-safe!
}
throw new Error('Invalid data shape');
}
// For API calls, define response types
type ApiResponse = {
data: UserData;
status: 'success' | 'error';
};
async function fetchApi(): Promise<ApiResponse> {
const response = await fetch('/api/data');
const data: unknown = await response.json();
if (isApiResponse(data)) {
return data;
}
throw new Error('Invalid API response');
}
function isApiResponse(data: unknown): data is ApiResponse {
return typeof data === 'object' && data !== null && 'status' in data && 'data' in data;
}
Never Use @ts-ignore or @ts-expect-error
ALWAYS fix the underlying type issue instead of suppressing errors.
WRONG:
// @ts-ignore
const value = user.profile.settings.theme;
// @ts-expect-error - API types are wrong
const data = await fetchUser();
CORRECT:
// Use optional chaining
const value = user?.profile?.settings?.theme;
// Fix the API types
type User = {
id: string;
profile: {
settings: {
theme: 'light' | 'dark';
};
};
};
const data = (await fetchUser()) as User; // Only if runtime shape is guaranteed
// Better: Validate at runtime with Zod
Type Narrowing Over Assertions
ALWAYS narrow types with type guards instead of using as assertions.
WRONG:
function processInput(input: string | number) {
const str = input as string;
return str.toUpperCase();
}
function handleEvent(event: Event) {
const clickEvent = event as MouseEvent;
console.log(clickEvent.clientX);
}
CORRECT:
function processInput(input: string | number) {
if (typeof input === 'string') {
return input.toUpperCase();
}
return input.toString().toUpperCase();
}
function handleEvent(event: Event) {
if (event instanceof MouseEvent) {
console.log(event.clientX); // Type-safe
}
}
// For custom type guards
function isMouseEvent(event: Event): event is MouseEvent {
return event instanceof MouseEvent;
}
function handleEvent(event: Event) {
if (isMouseEvent(event)) {
console.log(event.clientX);
}
}
Discriminated Unions for State
ALWAYS use discriminated unions for complex state and API responses.
WRONG:
type RequestState = {
loading: boolean;
error?: string;
data?: User;
};
// Impossible states are possible:
const state: RequestState = {
loading: true,
error: 'Failed',
data: user, // Can have error AND data!
};
CORRECT:
type RequestState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: User }
| { status: 'error'; error: string };
// Only valid states are possible
function handleState(state: RequestState) {
switch (state.status) {
case 'idle':
return 'Not started';
case 'loading':
return 'Loading...';
case 'success':
return state.data.name; // data is guaranteed
case 'error':
return state.error; // error is guaranteed
}
}
Use satisfies for Type Validation
Use satisfies to validate types without widening.
WRONG:
const config = {
endpoint: '/api/users',
timeout: 5000,
retries: 3,
}; // Type is inferred as { endpoint: string, timeout: number, retries: number }
// OR
const config: Config = {
endpoint: '/api/users',
timeout: 5000,
retries: 3,
}; // Type is widened to Config, loses literal types
CORRECT:
type Config = {
endpoint: string;
timeout: number;
retries: number;
};
const config = {
endpoint: '/api/users',
timeout: 5000,
retries: 3,
} satisfies Config;
// config.endpoint has type '/api/users' (literal), not string
// config.timeout has type 5000 (literal), not number
// But TypeScript validates the shape matches Config
Const Assertions for Literals
Use const assertions to preserve literal types.
WRONG:
const colors = ['red', 'green', 'blue']; // Type: string[]
const config = { apiUrl: 'https://api.example.com' }; // Type: { apiUrl: string }
CORRECT:
const colors = ['red', 'green', 'blue'] as const; // Type: readonly ['red', 'green', 'blue']
type Color = (typeof colors)[number]; // Type: 'red' | 'green' | 'blue'
const config = {
apiUrl: 'https://api.example.com',
} as const;
// Type: { readonly apiUrl: 'https://api.example.com' }
Branded Types for Domain Modeling
Use branded types to prevent mixing incompatible values.
WRONG:
type UserId = string;
type ProductId = string;
function getUser(id: UserId): User {
/* ... */
}
function getProduct(id: ProductId): Product {
/* ... */
}
const userId: UserId = '123';
const productId: ProductId = '456';
getUser(productId); // Oops! No error, both are just strings
CORRECT:
// Branded types
type UserId = string & { readonly __brand: 'UserId' };
type ProductId = string & { readonly __brand: 'ProductId' };
// Constructors
function createUserId(id: string): UserId {
return id as UserId;
}
function createProductId(id: string): ProductId {
return id as ProductId;
}
function getUser(id: UserId): User {
/* ... */
}
function getProduct(id: ProductId): Product {
/* ... */
}
const userId = createUserId('123');
const productId = createProductId('456');
getUser(productId); // Error! Type 'ProductId' is not assignable to type 'UserId'
Readonly by Default
ALWAYS use Readonly for data that shouldn't be mutated.
WRONG:
type Config = {
apiUrl: string;
timeout: number;
};
function init(config: Config) {
config.apiUrl = 'changed'; // Oops! Mutated
}
CORRECT:
type Config = {
readonly apiUrl: string;
readonly timeout: number;
};
// Or use Readonly utility
type Config = Readonly<{
apiUrl: string;
timeout: number;
}>;
function init(config: Config) {
config.apiUrl = 'changed'; // Error! Cannot assign to readonly property
}
// For arrays
function processItems(items: readonly string[]) {
items.push('new'); // Error! push doesn't exist on readonly array
return items.map((x) => x.toUpperCase()); // OK, map returns new array
}
Code Style and Patterns
Interface vs Type
Use interface for object shapes, type for unions, intersections, and utilities.
CORRECT:
// Interface for object shapes (can be extended)
interface User {
id: string;
name: string;
email: string;
}
interface Admin extends User {
permissions: string[];
}
// Type for unions
type Status = 'idle' | 'loading' | 'success' | 'error';
// Type for intersections
type AdminUser = User & { permissions: string[] };
// Type for mapped types
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Type for function signatures
type EventHandler = (event: Event) => void;
WRONG:
// Don't use type for simple object shapes that might be extended
type User = {
id: string;
name: string;
};
// Don't use interface for unions (not possible)
interface Status {
/* Can't do this */
}
No Enums - Use Union Types
NEVER use enums. Use const objects or union types instead.
WRONG:
enum Color {
Red = 'RED',
Green = 'GREEN',
Blue = 'BLUE',
}
// Problems:
// - Runtime overhead
// - Can't use as const context
// - Awkward type inference
CORRECT:
// Union type
type Color = 'red' | 'green' | 'blue';
// Or const object for values
const Color = {
Red: 'red',
Green: 'green',
Blue: 'blue',
} as const;
type Color = (typeof Color)[keyof typeof Color];
// Usage
function setColor(color: Color) {
console.log(color);
}
setColor(Color.Red); // OK
setColor('red'); // OK
setColor('yellow'); // Error
Named Exports Over Default Exports
ALWAYS use named exports for better refactoring and tree-shaking.
WRONG:
// Button.tsx
export default function Button() {
/* ... */
}
// Usage
import Button from './Button'; // Name can be anything
import Btn from './Button'; // Easy to create inconsistency
CORRECT:
// Button.tsx
export function Button() {
/* ... */
}
export type ButtonProps = {
/* ... */
};
// Usage
import { Button, type ButtonProps } from './Button';
// Name is fixed, refactoring is easier
Barrel Exports Sparingly
Use barrel exports only for public API, not for internal modules.
WRONG:
// src/components/index.ts - exports everything
export * from './Button';
export * from './Input';
export * from './Select';
export * from './internal/Helper';
export * from './internal/Utils';
// Creates circular dependencies and bundles everything
CORRECT:
// src/components/index.ts - only public API
export { Button, type ButtonProps } from './Button';
export { Input, type InputProps } from './Input';
export { Select, type SelectProps } from './Select';
// internal/* modules are not exported
// Import directly when needed: import { Helper } from './components/internal/Helper';
Use import type for Type-Only Imports
ALWAYS use import type for types to enable better tree-shaking.
WRONG:
import { User, formatUser } from './user';
// If you only use User as a type, it still bundles the module
type Response = {
user: User;
};
CORRECT:
import { formatUser } from './user';
import type { User } from './user';
type Response = {
user: User;
};
// Or inline
import { type User, formatUser } from './user';
Configure TypeScript to enforce this:
{
"compilerOptions": {
"verbatimModuleSyntax": true
}
}
Async/Await Over Promises
ALWAYS use async/await instead of promise chains.
WRONG:
function fetchUserData(id: string) {
return fetch(`/api/users/${id}`)
.then((response) => {
if (!response.ok) {
throw new Error('Failed to fetch');
}
return response.json();
})
.then((data) => {
return processUser(data);
})
.catch((error) => {
console.error(error);
throw error;
});
}
CORRECT:
async function fetchUserData(id: string): Promise<User> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error('Failed to fetch');
}
const data: unknown = await response.json();
if (!isUserData(data)) {
throw new Error('Invalid user data');
}
return processUser(data);
} catch (error) {
console.error(error);
throw error;
}
}
Runtime Validation with Zod
For external data (API responses, user input, env vars), ALWAYS validate at runtime.
WRONG:
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json(); // Assumes correct shape
}
CORRECT:
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
age: z.number().int().positive(),
});
type User = z.infer<typeof UserSchema>;
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data: unknown = await response.json();
// Runtime validation
const user = UserSchema.parse(data); // Throws on invalid data
return user;
}
// For environment variables
const EnvSchema = z.object({
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(20),
PORT: z.coerce.number().default(3000),
});
export const env = EnvSchema.parse(process.env);
Tooling Configuration
ESLint Flat Config
ALWAYS use ESLint flat config (eslint.config.js) for ESLint 9+.
CORRECT:
// eslint.config.js
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['dist', 'node_modules', 'coverage'],
},
js.configs.recommended,
...tseslint.configs.strictTypeChecked,
...tseslint.configs.stylisticTypeChecked,
{
languageOptions: {
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/ban-ts-comment': 'error',
},
}
);
Prettier Configuration
Use Prettier for formatting, ESLint for code quality.
CORRECT:
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "always"
}
Vitest Over Jest
ALWAYS use Vitest for new projects.
CORRECT:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node', // or 'jsdom' for React
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'dist/', '**/*.config.*'],
},
},
});
Use tsx for Running TypeScript
Use tsx for development, not ts-node.
CORRECT:
{
"scripts": {
"dev": "tsx watch src/server.ts",
"start": "node dist/server.js"
}
}
WRONG:
{
"scripts": {
"dev": "ts-node src/server.ts"
}
}
Function and Variable Patterns
Explicit Return Types for Exported Functions
ALWAYS add explicit return types to exported functions.
WRONG:
export function calculateTotal(items: Item[]) {
return items.reduce((sum, item) => sum + item.price, 0);
}
CORRECT:
export function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
export async function fetchUser(id: string): Promise<User> {
// ...
}
Use Function Declarations Over Arrow Functions for Top-Level
For top-level functions, prefer function declarations.
CORRECT:
export function formatUser(user: User): string {
return `${user.name} <${user.email}>`;
}
// Arrow functions are good for callbacks and short inline functions
const names = users.map((user) => user.name);
WRONG:
export const formatUser = (user: User): string => {
return `${user.name} <${user.email}>`;
};
Avoid Optional Parameters - Use Overloads or Separate Functions
For complex functions, use overloads instead of optional parameters.
WRONG:
function fetchData(id: string, options?: FetchOptions, callback?: Callback) {
// Complex logic handling all combinations
}
CORRECT:
// Separate functions
function fetchData(id: string): Promise<Data> {
/* ... */
}
function fetchDataWithOptions(id: string, options: FetchOptions): Promise<Data> {
/* ... */
}
// Or overloads
function fetchData(id: string): Promise<Data>;
function fetchData(id: string, options: FetchOptions): Promise<Data>;
function fetchData(id: string, options?: FetchOptions): Promise<Data> {
// Implementation
}
Generic Type Patterns
Constrain Generics Appropriately
ALWAYS constrain generics to the minimum required type.
WRONG:
function getProperty<T>(obj: T, key: string) {
return obj[key]; // Error: Type 'string' can't be used to index type 'T'
}
CORRECT:
function getProperty<T extends Record<string, unknown>, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// Usage
const user = { name: 'Alice', age: 30 };
const name = getProperty(user, 'name'); // Type: string
const age = getProperty(user, 'age'); // Type: number
Use Type Inference Where Possible
Let TypeScript infer types when obvious, be explicit when needed for API surface.
CORRECT:
// Let inference work
const numbers = [1, 2, 3]; // Inferred as number[]
const doubled = numbers.map((n) => n * 2); // Inferred as number[]
// Be explicit for API surface
export function processItems(items: string[]): ProcessedItem[] {
return items.map((item) => ({ value: item }));
}
Utility Types for Transformations
Use built-in utility types for common transformations.
CORRECT:
type User = {
id: string;
name: string;
email: string;
password: string;
};
// Make all properties optional
type PartialUser = Partial<User>;
// Pick specific properties
type UserCredentials = Pick<User, 'email' | 'password'>;
// Omit specific properties
type PublicUser = Omit<User, 'password'>;
// Make all properties readonly
type ImmutableUser = Readonly<User>;
// Make all properties required
type RequiredUser = Required<PartialUser>;
// Extract from union
type Status = 'idle' | 'loading' | 'success' | 'error';
type SuccessOrError = Extract<Status, 'success' | 'error'>; // 'success' | 'error'
// Exclude from union
type LoadingStates = Exclude<Status, 'success' | 'error'>; // 'idle' | 'loading'
Error Handling Patterns
Result Type for Expected Errors
Use Result type instead of throwing for expected errors.
WRONG:
function parseJson(text: string): unknown {
return JSON.parse(text); // Throws on invalid JSON
}
// Caller has to remember to try/catch
try {
const data = parseJson(input);
} catch {
// Handle error
}
CORRECT:
type Result<T, E> = { success: true; value: T } | { success: false; error: E };
function parseJson(text: string): Result<unknown, string> {
try {
const value = JSON.parse(text);
return { success: true, value };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
// Caller is forced to handle both cases
const result = parseJson(input);
if (result.success) {
console.log(result.value);
} else {
console.error(result.error);
}
Use never for Exhaustive Checking
Use never type to ensure all cases are handled.
CORRECT:
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; size: number }
| { kind: 'rectangle'; width: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.size ** 2;
case 'rectangle':
return shape.width * shape.height;
default:
// If we add a new shape and forget to handle it, this will error
const exhaustive: never = shape;
throw new Error(`Unhandled shape: ${exhaustive}`);
}
}
Module and Path Configuration
Use Path Aliases
Configure path aliases for cleaner imports.
CORRECT tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@utils/*": ["./src/utils/*"]
}
}
}
Usage:
// Instead of
import { Button } from '../../../components/Button';
// Use
import { Button } from '@components/Button';
Module Type for ESM
Set "type": "module" in package.json for ESM projects.
CORRECT:
{
"type": "module",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Bundler"
}
}
Summary Checklist
When writing or reviewing TypeScript code, ensure:
- Strict mode enabled with all strict flags
- No
anytypes - useunknownand narrow - No
@ts-ignoreor@ts-expect-errorwithout fix - Discriminated unions for complex state
- Type guards for runtime type checking
satisfiesoperator for type validation- Const assertions for literals
- Branded types for domain modeling
Readonlyfor immutable data- Named exports over default exports
import typefor type-only imports- Async/await over promise chains
- Runtime validation with Zod for external data
- ESLint flat config configured
- Vitest for testing
- Explicit return types on exported functions
- Appropriate generic constraints
- Result type for expected errors
nevertype for exhaustive checking- Path aliases configured
These conventions ensure type-safe, maintainable TypeScript code.
Similar Skills
Activates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.