standards-typescript/SKILL.md
TypeScript strict-mode best practices. Auto-load when editing *.ts or *.tsx files. Covers type safety patterns, Zod integration, Prisma types, and React typing conventions.
npx skillsauth add paulund/skills standards-typescriptInstall 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.
tsconfig.json (no "strict": false, no loosening noImplicitAny)any — use unknown and narrow it, or find the actual type@ts-ignore — use @ts-expect-error with a comment justifying why, if truly unavoidableFunction, Object, or {} as types — use specific signatures/interfaces// BAD
function run(fn: Function) { fn() }
const data: Object = {}
// GOOD
function run(fn: () => void) { fn() }
const data: Record<string, unknown> = {}
| Use interface for... | Use type for... |
| ------------------------------------------------------------------- | ------------------------------- |
| Object shapes (component props, API response shapes, domain models) | Unions and intersections |
| When you want extendability (declaration merging) | Mapped types, conditional types |
| | Primitive aliases |
interface Project { id: string; name: string; createdAt: Date } // Object shape
type PostStatus = 'draft' | 'scheduled' | 'posted' // Union
type AdminUser = User & { role: 'admin' } // Intersection
Prefer string literal unions — simpler, tree-shakeable, and work well with Prisma/Zod.
// BAD
enum PostStatus { Draft = 'draft' }
// GOOD
type PostStatus = 'draft' | 'scheduled' | 'posted' | 'failed'
as const for Config Objectsconst PLATFORMS = ['x', 'linkedin', 'substack'] as const
type Platform = (typeof PLATFORMS)[number] // 'x' | 'linkedin' | 'substack'
type PostState =
| { status: 'draft'; content: string }
| { status: 'scheduled'; content: string; scheduledAt: Date }
| { status: 'posted'; content: string; postedAt: Date; url: string }
function handlePost(post: PostState) {
if (post.status === 'posted') {
console.log(post.url) // TypeScript knows url exists here
}
}
satisfies Instead of Type Assertions// BAD — loses inference
const config = { provider: 'anthropic', model: 'llama-3' } as AIConfig
// GOOD — validated AND inferred
const config = { provider: 'anthropic', model: 'llama-3' } satisfies AIConfig
Note: use Record<string, string> rather than satisfies for lookup tables indexed at runtime — satisfies narrows to a literal type that rejects arbitrary string keys.
Always derive TypeScript types from Zod schemas. Don't declare the same shape twice.
export const createPostSchema = z.object({
content: z.string().min(1).max(280),
projectId: z.string().uuid(),
scheduledAt: z.string().datetime().optional(),
})
export type CreatePostInput = z.infer<typeof createPostSchema>
Use Prisma.<Model>GetPayload<...> for query result shapes. Never re-declare them.
import type { Prisma } from '@prisma/client'
type Post = Prisma.PostGetPayload<Record<string, never>>
type PostWithProject = Prisma.PostGetPayload<{ include: { project: true } }>
Always type the resolved value. Never return Promise<any>.
async function getProject(id: string): Promise<Project | null> { ... }
import type { Project, Post } from '@prisma/client'
import type { CreatePostInput } from '@/lib/schemas/post'
interface ButtonProps { label: string; onClick: () => void; disabled?: boolean }
interface CardProps { children: React.ReactNode; className?: string }
function handleChange(e: React.ChangeEvent<HTMLInputElement>) { setValue(e.target.value) }
const inputRef = useRef<HTMLInputElement>(null)
function handleError(error: unknown) {
if (error instanceof Error) {
console.error(error.message)
} else if (typeof error === 'string') {
console.error(error)
} else {
console.error('Unknown error', error)
}
}
| Utility | Use |
| ---------------- | ------------------------------------ |
| Partial<T> | All properties optional |
| Required<T> | All properties required |
| Pick<T, K> | Keep only keys K |
| Omit<T, K> | Remove keys K |
| Record<K, V> | Object with key type K, value type V |
| ReturnType<F> | Return type of a function |
| Awaited<P> | Unwrap a Promise type |
| NonNullable<T> | Remove null and undefined |
When the same constant (lookup table, config object, union type) appears in more than one file, extract it to src/lib/ and import it. Client components can import shared constants directly from src/lib/ without receiving them as props.
// BAD — same map declared in multiple files
const statusBadge: Record<string, string> = { draft: 'bg-gray-100 text-gray-700' }
// GOOD — one source of truth in src/lib/status-badges.ts
export const postStatusBadge: Record<string, string> = { draft: 'bg-gray-100 text-gray-700' }
development
Use when the user wants to run the project's lint + types + build sequence as a gate before pushing, opening a PR, or merging. Invoked by chained dev skills between phases. Trigger phrases - "/quality-gate", "run the quality gate", "check it builds".
tools
Use when the user wants to verify a PR's feature works at runtime by booting the dev server, exercising the affected UI via Chrome DevTools MCP, and posting a screenshot summary back to the PR. Idempotent — skips if `verified` or `verify-failed` is already on the PR. Trigger phrases - "/pr-verify", "verify this PR", "runtime check the pr".
testing
Use when the user wants a security-focused review pass on a PR with findings actioned as commits on the same branch. Trigger phrases - "/pr-security-review", "security review and fix".
testing
Use when the user wants to open a pull request for an already-pushed branch that implements a specific issue. Idempotent — returns the existing PR if one is already open for the branch. Trigger phrases - "/pr-open", "open the pr", "create pr for this branch".