Vp typescript best practices
Reusable Agent Skills by VdustR
npx -y skills add VdustR/skills --skill vp-typescript-best-practicesAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when writing, reviewing, or refactoring TypeScript in .ts, .tsx, or .test-d.ts files; fixing TS errors; designing types; validating external data; reducing unsafe assertions; or choosing between inference, annotations, satisfies, generics, unions, Zod schemas, and type-level tests.
SKILL.md
8.1 KB, as published. Nobody here has run it
TypeScript Best Practices
High safety per line of TypeScript. Follow the repository's documented style, tsconfig, lint rules, and generated-code workflow first; use these guidelines as portable defaults when the repo does not already decide.
Fallback Style Preferences
Use these only when the repository has no conflicting convention:
- Prefer
Array<T>/ReadonlyArray<T>overT[]/readonly T[]for visible aliases and public types. - Prefer
interfacefor extensible object shapes; prefertypefor unions, tuples, mapped types, conditional types, and aliases. - Prefer type-only namespaces to colocate a function's
Options,Result, and overload contracts when the repo already permits namespaces. - Prefer descriptive,
T-prefixed generic names for public or complex APIs, such asTConfigandTItem; reserveT,K, andVfor tiny conventional helpers. - Treat acronyms as words in identifiers:
userId,ApiClient,HttpError.
Key Items
- Prefer inference until a boundary needs a contract. Let local variables and implementation details infer naturally; annotate exported APIs, callbacks, config objects, test fixtures, runtime boundaries, and values passed directly into another API.
- Type entrypoints from the consumer contract. Use
ComponentProps<typeof Button>["onClick"],Parameters<typeof fn>[0],ReturnType<typeof hook>, or a publicOptionstype instead of retyping the same structure by hand. - Use
satisfiesas a review anchor. It validates object shape while keeping useful literal types. Use it where future schema, union, or config drift should fail at compile time. - Avoid
asand!by default. Prefer annotations,satisfies, control-flow narrowing, runtime validation, and explicit null checks. If an escape hatch is unavoidable, keep it local and document why it is safe. - Validate external data at runtime. Network responses, storage, environment
variables, messages, user input, and bridge APIs enter as
unknown; parse or normalize once, then derive TypeScript types from that runtime schema. - Keep transform boundaries single-sourced. If multiple callers must normalize the same wire shape, extract one shared transform or parser instead of copying conversion logic.
- Preserve TypeScript narrowing. Keep discriminated objects intact when the relationship between flags and data matters. Promote nullable lookups once with a guard instead of scattering non-null assertions.
- Guard indexed access. Treat
array[index], dictionary lookups, and dynamic keys as possibly missing unless the type system proves otherwise. Prefer value-driven iteration when the index is not needed. - Make unions exhaustive. Branch on discriminants with
switchor explicit guards, then usesatisfies neverso new variants break the build. - Extract, do not redefine. Reuse types from schemas, generated clients, library APIs, class constructors, and existing values. Manual copies drift.
- Keep literal lists as runtime data. Prefer
as constarrays or schemas plus derived union types over TypeScriptenumwhen the repo uses strip-only or erasable syntax tooling. - Type-test complex types. Use colocated
*.test-d.tsfiles for generics, schema compatibility, overloads, and intentional negative cases. - Respect generated and third-party boundaries. Regenerate generated code or wrap third-party gaps locally. Do not spread one local workaround across the application.
Quick Reference
| Goal | Preferred pattern | Avoid |
|---|---|---|
| Callback or prop type | ComponentProps<typeof X>["onY"] | Rewritten event types |
| Config object | const value = {...} satisfies Options | as Options |
| Runtime input | Schema parse, type guard, normalization function | input as Model |
| Nullable lookup | const tryX = find(); if (!tryX) throw ...; const x = tryX | find()! |
| Array/dictionary lookup | Guard undefined, use for...of when possible | items[i]! |
| Union handling | Discriminant switch + value satisfies never | Silent default branch |
| Error subclass args | ConstructorParameters<typeof Error> | Copying constructor overloads |
| Generated client types | Parameters, ReturnType, indexed access | Handwritten copies |
| Type-only regression | *.test-d.ts + satisfies | Runtime tests for pure types |
| Escape hatch | Local cast with WHY/SCOPE/SAFETY comment | Repo-wide any plumbing |
| Public array aliases | Array<TItem> / ReadonlyArray<TItem> | Mixed syntax without repo reason |
| Public generic names | TConfig, TItem, TResult | Cryptic names in large APIs |
Core Patterns
Consumer Contract Typing
const onSelect: ComponentProps<typeof Select>["onChange"] = (value) => {
updateSelection(value);
};
const payload: Parameters<typeof submitUser>[0] = {
id: user.id,
name: user.name,
};
Use this when the variable exists mainly to feed another API. The type error then appears where the value is created, not later at the callsite.
Runtime Boundary Validation
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
});
type User = z.infer<typeof UserSchema>;
function parseUser(input: unknown): User {
return UserSchema.parse(input);
}
One runtime source of truth is safer than a handwritten User plus a separate
validator. When transforms apply, use z.input and z.output only to make that
boundary explicit.
Exhaustive Branching
function labelStatus(status: Status): string {
switch (status.kind) {
case "loading":
return "Loading";
case "success":
return "Done";
case "error":
return status.message;
default:
status satisfies never;
throw new Error(`Unhandled status: ${String(status)}`);
}
}
satisfies never is compile-time only. Throw or return deliberately when runtime
continuation would be unsafe.
Common Mistakes
| Mistake | Better move |
|---|---|
| Adding types everywhere before reading the local style | Follow repo config, then annotate only useful boundaries |
Using as const to compensate for missing shape validation | Add satisfies TargetType or contextual typing |
| Casting API data because "the backend returns this" | Parse the data or derive the type from the generated/schema source |
| Destructuring discriminated query/result objects too early | Keep the object intact until after narrowing |
| Silencing a third-party typing issue with a broad helper | Wrap the smallest bridge and document the safety argument |
Adding enum in a strip-only toolchain | Use a runtime list/schema and derive the union type |
| Duplicating a generated client's request or response type | Extract from the generated function, schema, or model |
References
- references/type-patterns.md - inference,
annotations,
satisfies, type extraction, generics, and local type grouping. - references/code-style.md - safe lookups, narrowing, indexed access, assertions, and escape-hatch hygiene.
- references/union-exhaustive.md -
discriminated unions, exhaustive checks, intentional no-op cases, and
Record<Union, Value>coverage. - references/type-testing.md -
*.test-d.tspatterns, bidirectionalsatisfies, and negative type tests. - references/setup.md - strict tsconfig defaults, project references, erasable syntax, and verification commands.
- references/branded-types.md - nominal IDs, units, validated strings, and safe branding factories.
- references/template-literals.md - string pattern types for routes, events, keys, and constrained values.