npx skills add ...
npx skills add alexander-danilenko/cortex-ai-skills --skill typescript
Apply these opinionated TypeScript conventions whenever writing or reviewing TypeScript: branded types for domain modeling, discriminated unions for state, `satisfies` over `as`, const objects instead of enums, type-only imports, and the strict tsconfig baseline beyond `strict: true`. Also use for monorepo project references and incremental build setup.
npx skills add alexander-danilenko/cortex-ai-skills --skill typescript
House conventions for TypeScript 5+. Apply them to code you are writing or changing — don't refactor untouched files to match unless asked.
type UserId = string & { readonly __brand: 'UserId' }) and discriminated unions push errors to compile time, where they cost nothing. A runtime guard catches the same bug in production.satisfies over as. satisfies validates against a type while keeping the narrow inferred literal; as throws the check away. Reach for as only when you know something the compiler cannot, and say why in a comment.enum. as const objects are plain values with no emit and no nominal-typing surprises; enum generates runtime code and const enum breaks under isolatedModules.any is a bug report. Use unknown and narrow. If any is genuinely unavoidable at a boundary, isolate it in one adapter function rather than letting it spread through call sites.import type { … }). Bundlers erase them cleanly; mixed imports can keep a runtime dependency alive that you meant to drop..d.ts for anything consumed as a library (declaration: true). Without it, consumers fall back to any and your type work stops at the package boundary.composite. Incremental builds only rebuild what changed; a single flat tsconfig recompiles the world on every edit.Exhaustiveness only actually holds if you force it. A switch over a union compiles fine when a new variant appears — assertNever is what turns that into a build error at every site that needed updating:
A brand is only worth having if the sole way to obtain one is a function that validates:
The as inside the constructor is the one legitimate use — it's the boundary where validation converts an unverified value into a verified one. Casting to Email anywhere else defeats the whole type.
strict: true does not enable everything worth having. Add these — each one catches a class of bug the default set misses:
noUncheckedIndexedAccess is the highest-value one and the most disruptive to adopt: it makes arr[i] return T | undefined, which is the truth. Turn it on for new projects; for existing ones, expect a migration.
Module resolution: NodeNext when Node runs the output, bundler when a bundler does.
type Brand<T, B> = T & { readonly __brand: B };
type Email = Brand<string, "Email">;
export function toEmail(raw: string): Email {
if (!raw.includes("@")) throw new Error(`Not an email: ${raw}`);
return raw as Email;
}{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"isolatedModules": true,
"declaration": true,
"skipLibCheck": true
}
}