| name | typescript |
| slug | typescript |
| license | MIT |
| description | Provide best-practice coding conventions and generate standards-compliant TypeScript code.
|
TypeScript Style Guide Skill
Activation: This skill activates whenever the user says or implies TypeScript. It responds
with standards-compliant TypeScript code and can explain any rule on demand.
Table of Contents
- General Principles
- Naming Conventions
- Types & Interfaces
- Enums
- Variables & Constants
- Functions
- Classes
- Modules & Imports
- Generics
- Error Handling
- Async / Await & Promises
- Comments & Documentation
- Formatting & Style
- Null & Undefined Handling
- Type Assertions & Guards
- React & JSX (when applicable)
- Testing Conventions
- Tooling & Configuration
1. General Principles
- Strict mode always: Enable
"strict": true in tsconfig.json. This turns on noImplicitAny,
strictNullChecks, strictFunctionTypes, and more.
- Prefer readability over cleverness: Code is read far more often than written. Favour explicit,
self-documenting code.
- Minimise
any: Treat every use of any as tech debt. Prefer unknown when the type is
truly not known, or use generics.
- Immutability by default: Use
const over let; prefer readonly properties and
ReadonlyArray<T> / Readonly<T> utility types.
- Single responsibility: Each file, class, and function should have one clear purpose.
- Keep files small: A file should ideally stay under 400 lines. If it grows larger, consider
splitting it.
2. Naming Conventions
| Construct | Convention | Example |
|---|
| Variable / Function | camelCase | getUserName, isActive |
| Boolean variable | camelCase with prefix | isLoading, hasAccess, canEdit |
| Constant (module) | UPPER_SNAKE_CASE | MAX_RETRY_COUNT, API_BASE_URL |
| Constant (local) | camelCase | const defaultTimeout = 3000 |
| Class | PascalCase | UserService, HttpClient |
| Interface | PascalCase | UserProfile, ApiResponse |
| Type alias | PascalCase | UserId, Theme |
| Enum | PascalCase | Direction, HttpStatus |
| Enum member | PascalCase | Direction.Up, HttpStatus.Ok |
| Generic parameter | Single uppercase letter or descriptive PascalCase | T, TKey, TValue |
| File name | kebab-case.ts | user-service.ts, api-client.ts |
| Test file name | kebab-case.test.ts or kebab-case.spec.ts | user-service.test.ts |
Additional Naming Rules
- Do NOT prefix interfaces with
I (e.g., IUser → User).
- Do NOT suffix types/interfaces with
Type or Interface.
- Use descriptive names. Avoid single-letter variables except for conventional usages like loop
indices (
i, j) or generic type parameters (T, K, V).
- Acronyms of 2 characters stay uppercase (
IO, ID); 3+ characters use PascalCase
(Http, Xml, Api).
3. Types & Interfaces
Prefer interface for Object Shapes
interface User {
readonly id: string;
name: string;
email: string;
}
type Status = 'active' | 'inactive' | 'suspended';
type Result<T> = Success<T> | Failure;
When to Use type vs interface
Use interface when … | Use type when … |
|---|
| Defining the shape of an object or class | Creating union or intersection types |
| You want declaration merging | Using mapped / conditional types |
| Extending other interfaces | Aliasing primitive or tuple types |
Rules
4. Enums
Prefer String Enums
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
When to Use const Enum or Union Types
const enum HttpMethod {
Get = 'GET',
Post = 'POST',
Put = 'PUT',
Delete = 'DELETE',
}
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
Rules
- Do NOT use numeric enums without explicit values (auto-increment is fragile).
- Prefer string literal union types over enums when the set is small and no namespace features
are needed.
- Never mix string and numeric members in the same enum.
5. Variables & Constants
const maxRetries = 3;
const baseUrl = 'https://api.example.com';
let currentAttempt = 0;
var legacyValue = 'old';
let neverReassigned = 42;
Rules
- Always use
const unless the variable needs reassignment; then use let.
- Never use
var.
- Declare variables as close to their first usage as possible.
- One variable declaration per statement.
- Prefer destructuring for extracting properties:
const { name, age } = user;
const name = user.name;
const age = user.age;
- Use
as const for immutable literal objects / arrays:
const ROUTES = {
home: '/',
about: '/about',
contact: '/contact',
} as const;
6. Functions
Function Declarations
function calculateTotal(items: CartItem[]): number {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
const double = (n: number): number => n * 2;
function createUser(name: string, role: Role = Role.Viewer): User {
}
Rules
- Annotate return types for all exported / public functions.
- Prefer arrow functions for callbacks and inline functions.
- Prefer function declarations for top-level named functions (they are hoisted and more
readable in stack traces).
- Maximum parameters: 3. If more are needed, group them into an options object:
interface CreateUserOptions {
name: string;
email: string;
role?: Role;
department?: string;
}
function createUser(options: CreateUserOptions): User { ... }
function createUser(name: string, email: string, role: Role, dept: string): User { ... }
- Do not use
arguments; use rest parameters (...args) instead.
- Avoid
Function type. Use specific function signatures.
- Keep functions small — ideally under 30 lines of logic.
- Functions should do one thing.
- Prefer early returns to reduce nesting:
function getDiscount(user: ): {
(!user.) ;
(user. < ) ;
;
}
7. Classes
class UserService {
private readonly repository: UserRepository;
constructor(repository: UserRepository) {
this.repository = repository;
}
async findById(id: string): Promise<User | undefined> {
return this.repository.get(id);
}
}
Rules
8. Modules & Imports
export function parseConfig(raw: string): Config { ... }
export interface Config { ... }
export { parseConfig } from './parse-config';
export type { Config } from './parse-config';
Rules
9. Generics
function merge<TTarget, TSource>(target: TTarget, source: TSource): TTarget & TSource {
return { ...target, ...source };
}
function identity<T>(value: T): T {
return value;
}
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
Rules
- Use a single uppercase letter (
T, U, K, V) for simple generics.
- Use descriptive PascalCase prefixed with
T (TKey, TValue, TResult) when there are
multiple type parameters or the purpose is not obvious.
- Always add constraints when possible (
<T extends SomeType>).
- Avoid unnecessary generics — if a type parameter is used only once, it's likely not needed.
- Prefer generic utility types (
Array<T>, Promise<T>, Map<K, V>) over their shorthand where
readability benefits.
10. Error Handling
class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number = 500,
) {
super(message);
this.name = 'AppError';
}
}
function parseJson<T>(raw: string): T {
try {
return JSON.parse(raw) as T;
} catch (error) {
throw new AppError(
`Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`,
'PARSE_ERROR',
400,
);
}
}
Rules
11. Async / Await & Promises
async function fetchUser(id: string): Promise<User> {
const response = await httpClient.get<User>(`/users/${id}`);
return response.data;
}
async function fetchDashboard(userId: string): Promise<Dashboard> {
const [user, posts, notifications] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchNotifications(userId),
]);
return { user, posts, notifications };
}
Rules
- Prefer
async/await over .then() chains for readability.
- Always annotate return types as
Promise<T>.
- Use
Promise.all() for independent concurrent operations.
- Use
Promise.allSettled() when failures should not abort sibling operations.
- Never use
new Promise() when an async function suffices (avoid the explicit-construction
antipattern).
- Avoid
async void functions — they cannot be await-ed and swallow errors.
Exception: event handlers where the framework requires void.
- Prefer
for...of with await for sequential async iteration, not forEach.
12. Comments & Documentation
function compoundInterest(
principal: number,
rate: number,
times: number,
years: number,
): number {
return principal * Math.pow(1 + rate / times, times * years);
}
Rules
- Use TSDoc (
/** */) for all public APIs, exported functions, classes, interfaces, and types.
- Include
@param, @returns, @throws, and @example tags where applicable.
- Do NOT comment obvious code. The code should be self-documenting.
- Use
// TODO: with a ticket number for planned improvements.
- Use
// FIXME: for known issues that need resolution.
- Use
// HACK: for workarounds that should be revisited.
- Never leave commented-out code in the main branch.
- Place file-level comments at the top if the module's purpose is not obvious from its name.
- Keep comments up-to-date with code changes.
13. Formatting & Style
General
- Indentation: 2 spaces (no tabs).
- Semicolons: Required at the end of every statement.
- Quotes: Single quotes (
') for strings; backticks (`) for template literals.
- Trailing commas: Always use in multi-line constructs (arrays, objects, parameters, generics).
- Max line length: 100 characters (soft limit), 120 characters (hard limit).
- Braces: Required for all control structures, even single-line bodies.
- Blank lines: One blank line between top-level declarations; no multiple consecutive blank lines.
Specific Patterns
if (isValid) {
process();
}
if (isValid) process();
const config = {
host: 'localhost',
port: 3000,
debug: true,
};
const name = 'Alice';
const user = { name, age: 30 };
Tooling
- Use Prettier for auto-formatting with the following baseline config:
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"arrowParens": "always"
}
- Use ESLint with
@typescript-eslint/eslint-plugin and
@typescript-eslint/parser for linting.
14. Null & Undefined Handling
const city = user?.address?.city;
const displayName = user.nickname ?? user.name ?? 'Anonymous';
function getUser(id: string): User {
const user = repository.findById(id);
if (user === undefined) {
throw new AppError(`User not found: ${id}`, 'USER_NOT_FOUND', 404);
}
return user;
}
Rules
- Enable
strictNullChecks (included in strict mode).
- Prefer
undefined over null as the "absence of value" indicator, unless interacting with
external APIs that use null.
- Use optional chaining (
?.) and nullish coalescing (??) instead of manual checks.
- Do NOT use non-null assertion (
!) unless you can prove the value is always defined (add a
comment explaining why).
- Prefer the
satisfies operator (TS 5.0+) to validate types without widening.
15. Type Assertions & Guards
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value
);
}
interface Success<T> {
kind: 'success';
data: T;
}
interface Failure {
kind: 'failure';
error: Error;
}
type Result<T> = Success<T> | Failure;
function handleResult<T>(result: Result<T>): T {
switch (result.kind) {
case 'success':
return result.data;
case 'failure':
throw result.error;
}
}
Rules
- Prefer type guards (
is keyword) over type assertions (as).
- Use
as const for literal narrowing, not as SpecificType.
- Use discriminated unions with a
kind / type field for state machines and result types.
- Avoid
as unknown as T double assertions — they are almost always a design smell.
- When assertions are necessary, add a comment justifying them.
- Use
satisfies for type validation without assertion:
const palette = {
red: [255, 0, 0],
green: '#00ff00',
} satisfies Record<string, string | number[]>;
16. React & JSX (when applicable)
interface UserCardProps {
readonly user: User;
readonly onSelect?: (userId: string) => void;
}
export function UserCard({ user, onSelect }: UserCardProps): React.ReactElement {
const handleClick = useCallback(() => {
onSelect?.(user.id);
}, [onSelect, user.id]);
return (
<div className="user-card" onClick={handleClick} role="button" tabIndex={0}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}
Rules
- Use function declarations or named function expressions for components (not anonymous).
- Define props as an
interface (e.g., UserCardProps), not inline.
- Mark all props as
readonly.
- Do NOT use
React.FC — it is discouraged since React 18.
- Use
React.ReactElement or React.ReactNode as return type annotation.
- Colocate styles, tests, and types with the component when practical.
- Prefer controlled components over uncontrolled.
- Memoize expensive computations with
useMemo, stable callbacks with useCallback.
- Extract custom hooks when logic is reused across components.
- Component file structure:
- Imports
- Types / Interfaces
- Constants
- Component function
- Helper functions (private to the module)
17. Testing Conventions
describe('UserService', () => {
describe('findById', () => {
it('should return the user when the id exists', async () => {
const user = await service.findById('user-1');
expect(user).toEqual(expect.objectContaining({ id: 'user-1' }));
});
it('should return undefined when the id does not exist', async () => {
const user = await service.findById('non-existent');
expect(user).toBeUndefined();
});
});
});
Rules
18. Tooling & Configuration
Recommended tsconfig.json (Baseline)
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"declarationMap":
Recommended ESLint Rules
| Rule | Setting |
|---|
@typescript-eslint/no-explicit-any | error |
@typescript-eslint/explicit-function-return-type | warn (for exported functions) |
@typescript-eslint/no-unused-vars | error |
@typescript-eslint/consistent-type-imports | error |
@typescript-eslint/no-non-null-assertion | warn |
@typescript-eslint/prefer-nullish-coalescing | error |
@typescript-eslint/prefer-optional-chain | error |
@typescript-eslint/strict-boolean-expressions | warn |
@typescript-eslint/naming-convention | error (configured per construct) |
Usage
Natural Language Activation
Simply mention TypeScript in your conversation — the skill activates automatically.
Example prompts:
| Prompt | Skill Response |
|---|
| "Write a TypeScript function to merge two objects deeply" | Generates a fully typed, standards-compliant deepMerge<T, U>() function following all rules above. |
| "Review my TypeScript code for style issues" | Analyses the provided code against this guide and suggests improvements. |
| "Convert this JavaScript to TypeScript" | Converts code, adds strict types, interfaces, and follows all naming / formatting conventions. |
| "What's the TypeScript best practice for error handling?" | Explains the Result pattern, custom error classes, and typed catch blocks per §10. |
| "Create a TypeScript React component for a data table" | Generates a well-typed functional component following §16 React rules with proper props interface. |
| "Set up a new TypeScript project" | Provides tsconfig.json, ESLint config, Prettier config, and project structure following §18. |
Inline Rule References
You can ask about any specific section:
- "Explain TypeScript naming conventions" → References §2
- "How should I handle null in TypeScript?" → References §14
- "Show me TypeScript generic best practices" → References §9