skills/typescript-conventions/SKILL.md
TypeScript coding conventions for writing, reviewing, and refactoring typed code. Use when working on `.ts`, `.tsx`, or files that embed TypeScript such as Vue, Astro, or Svelte components. Also use for TypeScript snippets, typed refactors, and review comments about code organization or function structure.
npx skillsauth add perdolique/workflow typescript-conventionsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Apply conventions from the documentation, AGENTS.md files, lint rules, and configuration of the repository containing the active file first. Then use this skill for general TypeScript guidance, including TypeScript embedded in other file formats. Combine it with more specific framework, testing, or library-focused skills when needed.
Do not pass function calls or other non-trivial expressions directly as arguments to another function. If an argument would be a call, awaited value, chained transform, inline conditional, or other derived expression, assign it to a clearly named const first and pass that variable instead.
Passing simple values directly is fine:
Only allow nesting for declarative validator or schema-builder DSL calls where the schema shape is the primary focus of the code. For example, Valibot calls such as v.string(), v.number(), v.optional(...), v.object(...), and v.pipe(...) are allowed to remain nested.
// Avoid nested calls in arguments
const value = getValue()
runTask(value)
// Avoid hiding async work inside another call
const dataPromise = loadData()
await withMinimumDelay(dataPromise, splashDelay)
// Avoid complex inline conditionals as arguments
const target = hasValue ? routes.primary : routes.fallback
replaceRoute(target)
// Allowed: declarative validator DSL
const reservationSchema = v.object({
slug: v.string(),
startDate: v.string(),
endDate: v.string()
})
Do not hide helper calls, transforms, or other non-trivial expressions directly inside if, while, ternaries, or early-return guards. If a condition depends on derived data, compute that data first, store it in a clearly named const, and then branch on that value.
This keeps guards easy to scan, makes intent explicit during review, and avoids repeating the same normalization or parsing work across adjacent branches.
Passing simple values directly in conditions is fine:
// Avoid helper calls hidden inside the condition
if (trimToUndefined(currentValue) !== undefined) {
return []
}
// Prefer naming the derived value first
const normalizedCurrentValue = trimToUndefined(currentValue)
if (normalizedCurrentValue !== undefined) {
return []
}
// Avoid complex transforms in guards
if (items.filter(isVisible).length === 0) {
return null
}
const visibleItems = items.filter(isVisible)
if (visibleItems.length === 0) {
return null
}
Do not create a function whose only job is to call another function and immediately return its result or await it without adding any behavior. If the wrapper does not name a real concept and does not own logic such as validation, branching, retries, mapping, instrumentation, or error handling, inline the call at the usage site or rename the underlying function instead.
Use a wrapper only when it creates a clearer domain boundary or adds behavior that the caller should not repeat.
// Avoid wrappers that add no behavior
function getPath() : string {
return buildPath()
}
const path = buildPath()
replaceRoute(path)
// Allowed: wrapper adds real behavior
async function loadUser() : Promise<User> {
const user = await fetchUser()
trackLoad('user')
return user
}
When creating a new runtime object from an existing object, list each property explicitly instead of spreading the source object into the result. This keeps the final object shape visible at the construction site, improves readability during review, and prevents unrelated properties from being copied into the new value.
Apply this when returning view models, API payloads, or other reshaped objects in production code. Prefer explicit property selection even when most fields currently match the source object.
When an object property depends on a helper call or other derived expression, compute that value in a named const before constructing the object. Do not hide non-trivial transforms inside object literal properties.
This rule is intentionally scoped to durable application code. In tests, fixtures, and other low-risk support code, use the form that keeps setup and assertions easiest to read. Object spread is fine there when it keeps the example concise.
// Avoid hiding the resulting shape behind a spread
const payload = {
...formState
}
// Prefer explicit object construction
const payload = {
description: formState.description,
isArchived: formState.isArchived,
name: formState.name
}
// Avoid helper calls hidden inside object properties
const suggestions = suggestionValues.map((value) => ({
label: getPhoneCodeLabel(value),
value
}))
// Prefer naming the derived property first
const suggestions = suggestionValues.map((value) => {
const label = getPhoneCodeLabel(value)
return {
label,
value
}
})
Keep interface declarations and named object type aliases to one level of named properties. When a property needs an object shape, extract that shape into a separate named interface and reference it from the parent type.
Apply this to direct object properties, arrays of objects, and nullable or union forms such as brand: { ... }, items: { ... }[], and brand: { ... } | null.
This rule covers type declarations only. It does not apply to runtime object literals, validator schemas, framework configuration objects, or library DSL values where the object shape is executable configuration rather than a reusable type contract.
// Avoid inline object shapes in interfaces
interface ItemDetailResponse {
brand: {
id: number;
name: string;
};
id: string;
}
// Prefer named nested contracts
interface ItemDetailBrand {
id: number;
name: string;
}
interface ItemDetailResponse {
brand: ItemDetailBrand;
id: string;
}
development
Plan and drive non-trivial coding work from ambiguous request to scoped implementation and verification. Use when the user asks to plan before coding, plan then implement, split work into iterations or PR-sized tasks, tackle a risky multi-file feature, refactor, migration, or recover after failed work. Do not use for simple one-step edits, commit or PR creation, pure framework/domain conventions, or repo-specific roadmap docs where a more specific planning skill applies.
development
Write and maintain Vitest unit tests for TypeScript code. Use when the user needs unit coverage for utilities, services, or stores, or asks for Vitest-based tests with mocks, spies, and assertions.
development
Create GitHub pull requests from code changes via API or generate PR content in chat. Use when user wants to create/open/submit PR, mentions pull request/PR/merge request/code review, or asks to show/generate/display/output PR content in chat (give me PR, PR to chat, send PR to chat, etc).
development
Write and maintain Playwright end-to-end tests for web apps. Use when the user asks for browser or E2E coverage, or for tests covering pages, routes, redirects, navigation, dialogs, authentication, or multi-step user flows, even if they do not explicitly mention Playwright. Also use for API mocking, fixtures, and Playwright-specific assertions.