Install
openclaw skills install typescript-skillsProvide best-practice coding conventions and generate standards-compliant TypeScript code.
openclaw skills install typescript-skillsActivation: This skill activates whenever the user says or implies TypeScript. It responds with standards-compliant TypeScript code and can explain any rule on demand.
"strict": true in tsconfig.json. This turns on noImplicitAny,
strictNullChecks, strictFunctionTypes, and more.any: Treat every use of any as tech debt. Prefer unknown when the type is
truly not known, or use generics.const over let; prefer readonly properties and
ReadonlyArray<T> / Readonly<T> utility types.| 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 |
| React component file | PascalCase.tsx | UserProfile.tsx |
| Private field | camelCase (no _ prefix) | private count: number |
I (e.g., IUserUser).Type or Interface.i, j) or generic type parameters (T, K, V).IO, ID); 3+ characters use PascalCase
(Http, Xml, Api).interface for Object Shapes// ✅ Good — use interface for object shapes
interface User {
readonly id: string;
name: string;
email: string;
}
// ✅ Good — use type for unions, intersections, mapped types
type Status = 'active' | 'inactive' | 'suspended';
type Result<T> = Success<T> | Failure;
type vs interfaceUse 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 |
type over interface for function signatures:
type Comparator<T> = (a: T, b: T) => number;
Record<K, V> instead of { [key: string]: V }.Partial<T>, Pick<T, K>, Omit<T, K>, Required<T>) to derive types
rather than duplicating.// ✅ Good — string enum for readability and debuggability
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
const Enum or Union Types// ✅ Good — const enum for zero-runtime-cost enums (no reverse mapping needed)
const enum HttpMethod {
Get = 'GET',
Post = 'POST',
Put = 'PUT',
Delete = 'DELETE',
}
// ✅ Also good — string literal union (simpler, tree-shakeable)
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
// ✅ Good
const maxRetries = 3;
const baseUrl = 'https://api.example.com';
let currentAttempt = 0;
// ❌ Bad — using var
var legacyValue = 'old';
// ❌ Bad — using let when value never changes
let neverReassigned = 42;
const unless the variable needs reassignment; then use let.var.// ✅ Good
const { name, age } = user;
// ❌ Bad
const name = user.name;
const age = user.age;
as const for immutable literal objects / arrays:
const ROUTES = {
home: '/',
about: '/about',
contact: '/contact',
} as const;
// ✅ Good — explicit return type for public functions
function calculateTotal(items: CartItem[]): number {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
// ✅ Good — arrow function for callbacks / short lambdas
const double = (n: number): number => n * 2;
// ✅ Good — use default parameters instead of optional + fallback
function createUser(name: string, role: Role = Role.Viewer): User {
// ...
}
// ✅ Good
interface CreateUserOptions {
name: string;
email: string;
role?: Role;
department?: string;
}
function createUser(options: CreateUserOptions): User { ... }
// ❌ Bad — too many positional parameters
function createUser(name: string, email: string, role: Role, dept: string): User { ... }
arguments; use rest parameters (...args) instead.Function type. Use specific function signatures.// ✅ Good
function getDiscount(user: User): number {
if (!user.isPremium) return 0;
if (user.yearsActive < 2) return 5;
return 10;
}
// ✅ Good
class UserService {
private readonly repository: UserRepository;
constructor(repository: UserRepository) {
this.repository = repository;
}
async findById(id: string): Promise<User | undefined> {
return this.repository.get(id);
}
}
readonly for properties that should not change after construction.public, protected, private) on every member._. TypeScript's private keyword is sufficient.# (ES private fields) when true runtime privacy is required.class Logger {
constructor(private readonly prefix: string) {}
}
class UserRepositoryImpl implements UserRepository { ... }
// ✅ Good — named exports
export function parseConfig(raw: string): Config { ... }
export interface Config { ... }
// ✅ Good — re-export barrel file (index.ts)
export { parseConfig } from './parse-config';
export type { Config } from './parse-config';
import type / export type for type-only imports to help bundlers tree-shake:
import type { User } from './user';
node:fs, node:path)react, lodash)@/utils, @/components)../)./)import * as) unless re-exporting a module namespace.index.ts) thin — do not add logic.// ✅ Good — descriptive when intent is ambiguous
function merge<TTarget, TSource>(target: TTarget, source: TSource): TTarget & TSource {
return { ...target, ...source };
}
// ✅ Good — single-letter when intent is obvious
function identity<T>(value: T): T {
return value;
}
// ✅ Good — constrained generics
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
T, U, K, V) for simple generics.T (TKey, TValue, TResult) when there are
multiple type parameters or the purpose is not obvious.<T extends SomeType>).Array<T>, Promise<T>, Map<K, V>) over their shorthand where
readability benefits.// ✅ Good — custom error class
class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number = 500,
) {
super(message);
this.name = 'AppError';
}
}
// ✅ Good — typed error handling
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,
);
}
}
catch blocks).Error for domain-specific errors.catch variable as unknown (default in TS 4.4+) and narrow before use.type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
finally blocks or use disposable patterns.// ✅ Good — async/await
async function fetchUser(id: string): Promise<User> {
const response = await httpClient.get<User>(`/users/${id}`);
return response.data;
}
// ✅ Good — parallel execution
async function fetchDashboard(userId: string): Promise<Dashboard> {
const [user, posts, notifications] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchNotifications(userId),
]);
return { user, posts, notifications };
}
async/await over .then() chains for readability.Promise<T>.Promise.all() for independent concurrent operations.Promise.allSettled() when failures should not abort sibling operations.new Promise() when an async function suffices (avoid the explicit-construction
antipattern).async void functions — they cannot be await-ed and swallow errors.
Exception: event handlers where the framework requires void.for...of with await for sequential async iteration, not forEach./**
* Calculate the compound interest for a principal amount.
*
* @param principal - The initial amount of money.
* @param rate - The annual interest rate (decimal, e.g. 0.05 for 5%).
* @param times - Number of times interest is compounded per year.
* @param years - Number of years the money is invested.
* @returns The total amount after compound interest.
*
* @example
* ```typescript
* const total = compoundInterest(1000, 0.05, 12, 10);
* // total ≈ 1647.01
* ```
*/
function compoundInterest(
principal: number,
rate: number,
times: number,
years: number,
): number {
return principal * Math.pow(1 + rate / times, times * years);
}
/** */) for all public APIs, exported functions, classes, interfaces, and types.@param, @returns, @throws, and @example tags where applicable.// TODO: with a ticket number for planned improvements.// FIXME: for known issues that need resolution.// HACK: for workarounds that should be revisited.') for strings; backticks (`) for template literals.// ✅ Good — braces always, even for single-line if
if (isValid) {
process();
}
// ❌ Bad
if (isValid) process();
// ✅ Good — trailing comma
const config = {
host: 'localhost',
port: 3000,
debug: true,
};
// ✅ Good — consistent object shorthand
const name = 'Alice';
const user = { name, age: 30 };
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"arrowParens": "always"
}
@typescript-eslint/eslint-plugin and
@typescript-eslint/parser for linting.// ✅ Good — optional chaining
const city = user?.address?.city;
// ✅ Good — nullish coalescing
const displayName = user.nickname ?? user.name ?? 'Anonymous';
// ✅ Good — explicit null checks for critical paths
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;
}
strictNullChecks (included in strict mode).undefined over null as the "absence of value" indicator, unless interacting with
external APIs that use null.?.) and nullish coalescing (??) instead of manual checks.!) unless you can prove the value is always defined (add a
comment explaining why).satisfies operator (TS 5.0+) to validate types without widening.// ✅ Good — type guard function
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value
);
}
// ✅ Good — discriminated union
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;
}
}
is keyword) over type assertions (as).as const for literal narrowing, not as SpecificType.kind / type field for state machines and result types.as unknown as T double assertions — they are almost always a design smell.satisfies for type validation without assertion:
const palette = {
red: [255, 0, 0],
green: '#00ff00',
} satisfies Record<string, string | number[]>;
// ✅ Good — functional component with explicit props type
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>
);
}
interface (e.g., UserCardProps), not inline.readonly.React.FC — it is discouraged since React 18.React.ReactElement or React.ReactNode as return type annotation.useMemo, stable callbacks with useCallback.// ✅ Good — descriptive test structure
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();
});
});
});
__tests__ directory.*.test.ts or *.spec.ts.expect calls are okay if they test one logical
assertion).jest.fn() or equivalent for spies/mocks; type them properly:
const mockFetch = jest.fn<Promise<User>, [string]>();
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": true,
"sourceMap": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
}
}
| 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) |
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. |
You can ask about any specific section: