TypeScript Standards
SkillSearchUse when writing, reviewing, or architecting TypeScript in app or library code. Don't use for test-only files, docs, CSS, or helper placement (that is a different search).
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the TypeScript Standards skill
What this skill tells your AI
The instructions your AI receives, as published by alemtuzlak/skills in skills/typescript-standards/SKILL.md and read by ahel’s review.
Public APIs must feel like JavaScript. Callers write JS. You hide the type work. App or library does not change this.
This skill does not decide where a helper file lives. It does not add debug flags. It does not load other skills.
When To Use
Load when writing or reviewing TypeScript (.ts, .tsx). App code and library code both use these rules.
Skip test-only files, docs, and CSS.
Hard Gates
- Call sites never write
<T>. If they must, the signature is wrong. Generic definitions are fine. - Never write overloads. One signature. Derive the return with inference and conditional types.
- No
as any,as unknown as X, or cast chains. If you needas, the types are usually wrong. as constonly to stop widening (e.g."open"staying"open", notstring). Not a habit.- Do not annotate function return types. Let TypeScript infer. Do not write
: Fooafter). - If the function cannot work without a value, that value is required. Do not take
T | undefinedand return early. Do not accept the property. - Pipe types through callers. If
AneedsB,Cthat callsAtakesB. Do not take a looser type inCand cast insideC. - Fix this skill’s smells only in the file you already opened. Do not tour the folder.
Inference
Design signatures so TypeScript infers from arguments and returned values.
createStore({ count: 0 })
not
createStore<{ count: number }>({ count: 0 })
Do not write the return type on the function. Infer it from return.
Types
- Before a new
typeorinterface, search the current package and neighbors. Use or extend the one that is the same shape. Do not grep the whole monorepo for a one-file type. - One signature. Conditional types when the return depends on the input. No overload list.
- Constrain with types, not with runtime
typeof/ extra unions.function f(b: string)notfunction f(b: string | undefined) { if (typeof b === "string") ... }. - Arrays stay constrained.
arr.map(a => ...)infersa. Do not writearr.map((a: Foo): Bar => ...). filter/ narrow with a type predicate:(value): value is X. Do not leave the array as a loose union after a filter that is meant to narrow.- Do not loosen types “just in case.” If the code cannot run without a field, that field is required on the argument.
Functions
- First arg is the required value (URL, input, id).
- Extra flags go in an options object when a lone boolean is unclear (
fn(url, true)) or when there is more than one extra.add(a, b)stays two values. - Options/setup must still work if pulled into a helper. If extraction breaks types or behavior, fix the signature. Do not extract every options object by default.
- Return the type the caller needs, including nullish. If the next function wants
X | null, returnX | null. Do not returnX | undefinedand thenx ?? null. Picknullorundefinedfor that path and keep it. - Name complex conditions:
const isOpenGold = order.status === "open" && order.tier === "gold"
if (isOpenGold) { /* ... */ }
Do not dump a long && / || chain into if (...).
- Early return to narrow. Prefer an early
ifover nestedelse. For several missing values, oneifwith||:
if (a === null || a === undefined || b === null) return
Then use a and b as defined. Do not write if (a !== null || a !== undefined) (that check is always true).
- Three or more branches on the same value: exhaustive
switch. Use aneverdefault (or no default with a union the compiler checks). Do not write a ladder ofif/else if. - Loops: assign the list first, then iterate.
const values = getValues(input)
for (const value of values) { /* ... */ }
Do not call getValues in the for header.
- Spreads stay flat.
{ ...a, b: { ...c } }. Do not convert fields in the spread (c.x === undefined ? null : c). If every call site converts format A to format B, the data model failed. Convert once at a boundary, or change the types so both sides match. - Public functions (package entry, exported SDK, what users call) get TSDoc: what it does, the arguments, how to call it. Do not annotate the return type in the signature. You can describe the result in the comment.
Casts
Banned: as any, as unknown as X, double casts, (x as any).code.
Allowed:
as constonly when a literal widens (string/number/array) and you need the literal type.return x as Tonly when TypeScript cannot infer a generic return (e.g.JSON.parseintoT). Still do not write: Ton the function.
Errors
Custom Error subclasses only at a boundary callers catch (exported function, SDK, route handler). Internal helpers can throw new Error(...). Document on that boundary (TSDoc) what can throw and which class to catch.
Barrels
No export *. Inside the repo, import from the real file. The package public entry (index.ts behind exports) can re-export named symbols only.
Procedures
Procedure 1: Before a new type or signature
- Search the current package and neighbors for an existing type.
- Write one function signature. Infer from args and from
return. No overloads. No call-site<T>. No return annotation. - If the body cannot run without a field, that field is required. Do not take it as optional.
- If this function calls another that needs type
B, this function takesB. - If extras are a magic boolean, use an options object.
- If the function is public, write TSDoc (what, args, how to use).
Procedure 2: While editing
- Do not add banned casts.
as constonly for widening. Do not add return type annotations. - Name long conditions. Early-return to narrow. Switch when there are 3+ branches on one value.
- Assign loop lists before
for. Keep spreads simple. Matchnullvsundefinedto the caller. - Direct imports. No new
export *. - If this file already has this skill’s smells, fix those in this file.
Procedure 3: Throws at a catchable boundary
- If callers are expected to
instanceofa specific error, use a custom class. - If the throw is internal only,
new Erroris enough.
Decision Tree
- Writing/reviewing
.ts/.tsx→ this skill. - Test file → not this skill.
- New type → Procedure 1 step 1.
- New function → Procedure 1.
- Optional arg plus
typeof/ early return because it is missing → Hard gate 6. Make it required, or do not take it. asneeded → Casts. If not an allowed case, fix the types.- Helper file home → not this skill.
debug/ logger on config → not this skill.
Red Flags
| Signal | What it means | Do instead |
|---|---|---|
fn<Foo>(...) at the call site | Signature does not infer | Change the definition so args carry the type. |
function fn(...): A plus two more function fn | Overloads | One signature, conditional return. |
function fn(): Order | Return type annotated | Drop : Order. Infer from return. |
as unknown as X inside a wrapper to call A | Types not piped | The wrapper takes the same type A takes. |
as unknown as X | Types were abandoned | Fix the types. |
"open" as const on every string | Habit | Use as const only when the type is otherwise string. |
fn(url, true) | Magic boolean | fn(url, { retry: true }). |
add({ b: 2 }) for two numbers | Options object overused | add(a, b). |
if (a.x === "a" && a.y === 2 || z) | Unnamed condition | const isReady = ... then if (isReady). |
b: string | undefined then typeof b === "string" | Runtime used as a type | b: string. |
id?: string then if (!id) return because the body needs id | Optional that cannot be optional | Required id: string. |
return x ?? null after X | undefined | Nullish mismatch | Return X | null from the start (or undefined if that is the contract). |
for (const x of getValues(a)) | Work hidden in the header | const values = getValues(a) then for. |
arr.map((a: Foo): Bar => | Types forced on the callback | Constrain arr. Let a infer. |
filter that still leaves T | undefined | No predicate | function isDefined<T>(v: T | undefined): v is T. |
Spread with === undefined ? null inside | Conversion in the spread | Spread as-is, or one mapper at a boundary. |
Three if (status === in a row | Missing exhaustive switch | switch (status) over the union. |
export * from './foo' | Barrel | Direct import, or named re-export at package entry. |
| Public export with no TSDoc | Users cannot read the contract | What it does, args, how to call it. |
| Custom class for an internal throw | Extra type | throw new Error(...). |
Creating packages/shared from this skill | Placement | Not this skill. |
Adding debug?: boolean to every config | Old rule | Not this skill. |
| Fixing casts in a file you did not open | Drive-by | Procedure 2, this file only. |
Error Handling
- Generic return cannot infer (
parse<T>/JSON.parse):return result as Tis the allowed exception. Do not write: Ton the function. Do not addas anyon the way there. - No existing type in package/neighbors: write one. Do not invent a parallel name for the same shape in this change.
- Conditional type cannot express the return: still no overloads. Split the function or narrow with a discriminated input. Still no return annotation.
- Callers must catch a specific failure: Procedure 3. Custom class at that boundary. TSDoc names the class.
- Downstream wants
null, this function returnsundefined: change this function’s return. Do not patch with??at every call site. - User interrupts that a type home is wrong: move it in this same change. Update imports in opened files.
See patterns.md for before/after code.
Signals
- GitHub stars
- 39
- Forks
- 1
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
typescript-standards- Source
- github.com/alemtuzlak/skills