skillv1.0.0
typescript-strict
Strict TypeScript patterns: unknown over any, discriminated unions, satisfies, narrowing
typescriptlanguage
Install
$npx autoskills --items typescript-strict
Or scan + install everything matching your stack with npx autoskills.
Source
View on GitHubStrict TypeScript
strict: true is non-negotiable. If it's off, the type system is doing far less than you think.
tsconfig.json baseline
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true
}
}noUncheckedIndexedAccess is the biggest correctness win after strict itself โ it makes arr[i] and obj[key] return T | undefined, catching the entire class of "I forgot to check for undefined" bugs.
unknown over any
any opts out of the type system. unknown says "I don't know what this is yet โ narrow before using."
// bad
function parse(json: string): any { return JSON.parse(json); }
// good
function parse(json: string): unknown { return JSON.parse(json); }
// caller must narrow:
const data = parse(input);
if (typeof data === 'object' && data && 'name' in data) { ... }For real parsing, use zod or valibot โ runtime validation that produces a typed result.
Discriminated unions over optional fields
// bad: every field is optional, no relationship enforced
interface Result {
status?: 'success' | 'error';
data?: User;
error?: string;
}
// good: status discriminates which other fields exist
type Result =
| { status: 'success'; data: User }
| { status: 'error'; error: string };
if (result.status === 'success') {
result.data; // typed as User, no narrowing needed
}satisfies for "shape conforms, type preserved"
// bad: widens the type to Record<string, Route>
const routes: Record<string, Route> = {
home: { path: '/' },
about: { path: '/about' },
};
routes.home; // Route โ keys are gone
// good: type-checked AND keys preserved
const routes = {
home: { path: '/' },
about: { path: '/about' },
} satisfies Record<string, Route>;
routes.home; // still has narrow type with literal path: '/'Use satisfies for config objects, route tables, enum-like maps.
Narrowing
typeoffor primitives:typeof x === 'string'.infor object shape:'data' in result.- Equality for literals:
if (status === 'success'). - Type predicates for custom narrowing:
function isUser(x: unknown): x is User { return typeof x === 'object' && x !== null && 'id' in x; } assertfor invariants:function assertNever(x: never): never { throw new Error(Unhandled: ${x}); }โ use in the default of a switch over a union to make exhaustiveness errors compile-time.
Errors
// bad: catch (e) gives you `unknown` โ but most code uses it as any
try { ... } catch (e) {
console.log(e.message); // type error under strict
}
// good
try { ... } catch (e) {
const message = e instanceof Error ? e.message : String(e);
console.log(message);
}Generics
- Constrain generics โ
<T extends object>, not<T>. Unconstrained generics often hide bugs. - Don't reach for generics first. Try function overloads. Try
as const. Try discriminated unions. Generics are powerful but expensive to read. - Name them meaningfully โ
Tis fine for one-off identity, butTUser/TItemreads better in larger functions.
What to avoid
ascasts โ they're assertions, not conversions. The compiler trusts you. If you need them, narrow with a predicate instead.anyin library boundaries โ if you can't avoid it internally, at minimum keepanyout of the public API.Function,Object,{}โ useless types. Use a real signature orRecord<string, unknown>.!non-null assertions โ same problem asas. The compiler can't help you. Narrow.- Stringly-typed flags โ
kind: stringshould bekind: 'a' | 'b' | 'c'.
Project shape
- One source of truth per concept โ don't redeclare interfaces across files. Export from a single types module.
- Branded types for IDs โ
type UserId = string & { readonly __brand: 'UserId' }. Catches "passed a project ID where a user ID was expected" at compile time. readonlyaggressively โ for arrays (ReadonlyArray<T>), objects (Readonly<T>), and class fields that don't change after construction.