๐Ÿ”งAutoSkills
All items
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.

Strict 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

  • typeof for primitives: typeof x === 'string'.
  • in for 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;
    }
  • assert for 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 โ€” T is fine for one-off identity, but TUser/TItem reads better in larger functions.

What to avoid

  • as casts โ€” they're assertions, not conversions. The compiler trusts you. If you need them, narrow with a predicate instead.
  • any in library boundaries โ€” if you can't avoid it internally, at minimum keep any out of the public API.
  • Function, Object, {} โ€” useless types. Use a real signature or Record<string, unknown>.
  • ! non-null assertions โ€” same problem as as. The compiler can't help you. Narrow.
  • Stringly-typed flags โ€” kind: string should be kind: '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.
  • readonly aggressively โ€” for arrays (ReadonlyArray<T>), objects (Readonly<T>), and class fields that don't change after construction.