ts-types
Skill1 starsChanged 27 days agoMITView on GitHub
TypeScript type system advisor. Always use this skill when designing TypeScript types, working with generics, narrowing union types, building discriminated unions, using utility types, or avoiding any. Use when the user says "how do I type this", "why is TypeScript complaining", "object is possibly null", "how do I narrow this", "how do I make this generic", "what's the right type for X", "how do I model this as a union type", or "how do I constrain a generic". Read this skill before answering any TypeScript type question.
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 4 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 28
Use any only at integration boundaries with untyped third-party code, and isolate it.
- Distinctive: line 45
When a value can be in multiple mutually exclusive states, use a discriminated union: a shared literal field that TypeScript uses to narrow.
- Distinctive: line 110
Constrain generics: when you need to access properties:
- Distinctive: line 118
Don't over-generify: If a function only ever deals with one type, a generic adds complexity without benefit.
- Distinctive: line 159
Use sparingly; they add complexity and slow compilation when overused.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
TypeScript Types
Good types are constraints, not descriptions. The goal is to make invalid states unrepresentable, so the compiler catches mistakes before they reach runtime.
Prefer unknown over any
any disables type checking. unknown preserves it.
// Bad: silences all errors downstream
function parse(raw: any) {
return raw.data.value; // no error, even if raw is null
}
// Good: forces the caller to narrow before using
function parse(raw: unknown) {
if (typeof raw === 'object' && raw !== null && 'data' in raw) {
// now safe to use
}
}Use any only at integration boundaries with untyped third-party code, and isolate it; don't let it propagate.
Avoid type assertions (as)
as T tells the compiler to trust you. It doesn't check. Use it only when you genuinely know something the compiler can't infer, and add a comment explaining why.
// Bad: silences a real error
const el = document.getElementById('app') as HTMLDivElement;
// Better: narrow properly
const el = document.getElementById('app');
if (!(el instanceof HTMLDivElement)) throw new Error('Missing #app');Discriminated unions for modeling state
When a value can be in multiple mutually exclusive states, use a discriminated union: a shared literal field that TypeScript uses to narrow.
type Result<T> =
| { status: 'ok'; data: T }
| { status: 'error'; message: string }
| { status: 'loading' };
function render(result: Result<User>) {
if (result.status === 'ok') {
return result.data.name; // TypeScript knows data exists here
}
if (result.status === 'error') {
return result.message; // TypeScript knows message exists here
}
return 'Loading...';
}This beats optional fields (data?: T; error?: string) because optional fields allow illegal combinations like { data: x, error: y }.
Type narrowing
TypeScript narrows types based on control flow. Use the built-in narrowing tools before reaching for type assertions.
typeof x === 'string': primitive typesx instanceof MyClass: class instances'field' in x: object shapes- Custom type guards:
function isUser(x: unknown): x is User
function isUser(x: unknown): x is User {
return typeof x === 'object' && x !== null && 'id' in x && 'name' in x;
}Exhaustiveness checking: make the compiler tell you when you've missed a case:
function assertNever(x: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(x)}`);
}
function handle(result: Result<User>) {
switch (result.status) {
case 'ok': return result.data;
case 'error': return null;
case 'loading': return null;
default: return assertNever(result); // compile error if a case is added but not handled
}
}Generics
Generics let you write code that works across types without sacrificing type information. Use them when the relationship between types matters more than the specific type.
// Bad: loses the return type
function first(arr: unknown[]): unknown { return arr[0]; }
// Good: preserves it
function first<T>(arr: T[]): T | undefined { return arr[0]; }Constrain generics when you need to access properties:
function getId<T extends { id: string }>(item: T): string {
return item.id;
}Don't over-generify. If a function only ever deals with one type, a generic adds complexity without benefit.
Utility types
Know the built-ins before writing your own:
| Utility | What it does |
Partial<T> | All fields optional |
Required<T> | All fields required |
Readonly<T> | All fields readonly |
Pick<T, K> | Keep only keys K |
Omit<T, K> | Drop keys K |
Record<K, V> | Map from K to V |
ReturnType<F> | Return type of a function |
Parameters<F> | Parameter tuple of a function |
NonNullable<T> | Remove null and undefined |
The satisfies operator
satisfies validates that a value matches a type without widening the inferred type. Useful when you want both the type check and the precise literal types.
const config = {
port: 3000,
host: 'localhost',
} satisfies Record<string, string | number>;
// config.port is still inferred as 3000, not number
// but TypeScript validates the shape against Record<string, string | number>Template literal types
For string manipulation at the type level:
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickHandler = EventName<'click'>; // 'onClick'Use sparingly; they add complexity and slow compilation when overused.
What to watch for in code review
anythat propagates past an integration boundaryas Twithout a comment justifying it- Optional fields modeling mutually exclusive states (should be a discriminated union)
- Missing exhaustiveness checks in switch statements over union types
- Generics with no constraint accessing properties that might not exist
In this repository25 other files it gives its agents.
- api-designSkill
- arch-reviewSkill
- db-reviewSkill
- debugSkill
- dotnet-asyncSkill
- dotnet-opsSkill
- dotnet-testingSkill
- dotnet-typesSkill
- go-concurrencySkill
- go-opsSkill
- go-testingSkill
- go-typesSkill
- incidentSkill
- node-asyncSkill
- node-opsSkill
- perfSkill
- pr-prepSkill
- py-asyncSkill
- py-opsSkill
- py-testingSkill
- py-typesSkill
- refactorSkill
- securitySkill
- test-genSkill
- ts-testingSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

