
Typescript
- 4 installs
- 19 repo stars
- Updated August 1, 2026
- xobotyi/cc-foundry
Helps with ai & agent building tasks.
About
typescript is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- typescript
- AI & Agent Building
- AI-coding skill
Typescript by the numbers
- 4 all-time installs (skills.sh)
- Ranked #13,348 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/xobotyi/cc-foundry --skill typescriptAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4 |
|---|---|
| repo stars | ★ 19 |
| Last updated | August 1, 2026 |
| Repository | xobotyi/cc-foundry ↗ |
What it does
Helps with ai & agent building tasks.
Files
TypeScript
<prerequisite> This skill extends the JavaScript skill. You must load javascript first — naming, ternary operator rules, async patterns, and module conventions are defined there and not duplicated here. </prerequisite>
Types encode intent. Let the compiler prove the rest.
TypeScript's value is in catching bugs at compile time. Write types that express your domain; let inference handle the obvious. Never fight the type system — if you need as or any, the types are wrong.
References
| Topic | Reference | Contents |
|---|---|---|
| Generics, utility types, type-level programming | [${CLAUDE_SKILL_DIR}/references/generics.md] | Utility type tables, conditional/mapped type examples, infer, template literals |
| Narrowing, type guards, discriminated unions | [${CLAUDE_SKILL_DIR}/references/narrowing.md] | typeof/instanceof/in examples, exhaustive switch, type predicates, assertion fns |
| tsconfig options, module resolution, project setup | [${CLAUDE_SKILL_DIR}/references/configuration.md] | Base/strict/module configs, library setup, compiler directives, project structure |
| Branded types, overloads, class patterns, enums | [${CLAUDE_SKILL_DIR}/references/patterns.md] | Interface vs type examples, assertion patterns, enum anti-patterns, callback types |
Type Safety
- `strict: true` always. No exceptions. It enables
strictNullChecks,
noImplicitAny, strictFunctionTypes, and other critical checks.
- `unknown` over `any`. Use
unknownand narrow with type guards.anydisables
type checking entirely. Reserve any for migration or test mocks only — document why.
- No non-null assertions (`!`) without justification. Prefer narrowing. If
!is
truly needed, add a comment explaining why the value cannot be null.
- No type assertions (`as`) for object literals. Use type annotations (
: Foo)
instead — assertions hide missing/extra property errors.
unknown vs any Decision
| Situation | Use |
|---|---|
| Value from external source (API, JSON.parse, user input) | unknown |
| Function accepts anything, passes through without touching | unknown |
| Migrating JS to TS incrementally | any (temporary, with comment) |
| Test mock that intentionally bypasses type checking | any (with comment) |
The {} Type
{} means "any non-nullish value" — almost never what you want.
| Type | Allows |
|---|---|
unknown | Everything (null, undefined, primitives, objects) |
object | Non-primitive, non-null values |
{} | Any non-nullish value (primitives included) |
Record<string, unknown> | Objects with string keys |
Prefer unknown for opaque values, Record<string, unknown> for dict-like objects, object when you need "any non-primitive".
Type Annotations
- Omit trivially inferred types. Don't annotate
const x: number = 5or
const s: string = "hello". The compiler infers these correctly.
- Annotate complex return types. When inference produces opaque or wide types,
annotate explicitly for readability.
- Annotate function signatures at API boundaries. Exported functions and public
methods should have explicit parameter and return types.
- Use `import type` for type-only imports. Enforced by
verbatimModuleSyntax.
Use export type for type re-exports — required for isolatedModules.
- Annotate for precision with structural types. Annotate at declaration so errors
appear where the bug is, not at distant call sites.
Interfaces and Types
- `interface` for object shapes. Better error messages, IDE support, and performance.
- `type` for everything else — unions, intersections, tuples, function types,
mapped/conditional types.
- Decision rule: Object shape with known properties?
interface. Everything else?
type. Pick one pattern per kind and stay consistent within a project.
- No empty interfaces. Use a branded type or discriminated union as a marker.
- No `namespace`. Use ES modules.
namespaceis legacy. - No wrapper types.
stringnotString,numbernotNumber. - Use interfaces for data shapes, not classes. A class used purely as a data shape
adds unnecessary overhead.
Null Handling
- Prefer optional `?` over `| undefined` for fields and parameters.
| undefined
forces callers to pass undefined explicitly.
- Don't include `null`/`undefined` in type aliases. Keep nullability at the use site:
function getUser(): User | null not type MaybeUser = User | null.
- Null narrowing:
!= nullchecks both null and undefined (the one valid==use).
?. for optional access. ?? for defaults.
Generics
- Name type parameters descriptively when meaning is non-obvious.
Tis fine for
single-parameter generics; use TKey, TValue, TItem for multiple parameters.
- Constrain generics with
extendswhen possible.<T extends string>is better
than <T> if T must be a string.
- Keep generic constraints tight —
<T extends Record<string, unknown>>is better
than <T extends object> when you need string keys.
- Don't add unused type parameters. Every generic must appear in the signature.
- Avoid return-type-only generics. If a generic appears only in the return type, it
cannot be inferred and forces callers to guess.
- Let inference work. Don't specify type arguments when the compiler can infer them:
identity("hello") not identity<string>("hello").
- Use type parameters in constraints:
<T, K extends keyof T>to relate parameters. - Generic parameter defaults:
interface Container<T, U = T[]>— omitted type args
fall back to the default.
- `NoInfer<T>` (TS 5.4+) prevents a parameter from being an inference site — use when
a parameter should be constrained by other params, not drive inference.
Utility Types
Prefer built-in utility types over hand-rolling equivalents. Key types: Partial, Pick, Omit, Record, Exclude, Extract, ReturnType, Parameters, Awaited, NoInfer. Use explicit interfaces when the type represents a distinct domain concept. See ${CLAUDE_SKILL_DIR}/references/generics.md for the full catalog and usage guidance.
Conditional types (T extends U ? X : Y), mapped types ({ [P in keyof T]: ... }), and template literal types (` ${T}Changed ) are advanced tools — use for library code and framework types. See ${CLAUDE_SKILL_DIR}/references/generics.md for distributive behavior, infer`, modifier removal, and key remapping.
Complexity Budget
| Tier | Tools | Use When |
|---|---|---|
| Simple | interface, type alias, union | Always — default choice |
| Moderate | Partial, Pick, Omit, Record | Well-known transformations |
| Advanced | Conditional, mapped, template literal | Library code, framework types |
| Expert | Recursive types, complex infer chains | Rarely — last resort |
Stay at the lowest tier that solves your problem. If you can't explain what a type does in one sentence, it's too complex — split it, simplify it, or use explicit interfaces.
Narrowing
- Prefer discriminated unions for variant types. Add a
kindortypeliteral
field to each variant.
- Use exhaustive switches. Add `default: { const _exhaustive: never = value;
return _exhaustive; }` to catch unhandled variants at compile time.
- Type predicates for reusable guards:
function isFish(pet: Animal): pet is Fish.
Use when filtering arrays or in multiple call sites.
- Assertion functions: `function assertIsError(value: unknown): asserts value is
Error` — use for validation at boundaries.
- `typeof`, `instanceof`, `in` — use JavaScript narrowing constructs; TypeScript
understands them natively.
- `typeof null === "object"` pitfall. Always check for
nullseparately before
typeof object checks.
- `in` operator and optional properties: if
Humanhasswim?,"swim" in animal
narrows to Fish | Human, not just Fish.
- Truthiness narrowing pitfall: fails on
"",0,NaN,false. Prefer explicit
null checks over truthiness when these values are valid.
- Equality narrowing:
x === ynarrows both to their common type.!= nullchecks
both null and undefined.
Enums
- Prefer union types over enums when values are simple string literals:
type Status = "active" | "inactive" is simpler than enum Status.
- Prefer string enums over numeric enums when an enum is needed. String enums have
meaningful runtime values and readable debug output.
- Never use numeric enums with implicit values — always assign explicit values.
- Never coerce enums to booleans. Compare explicitly:
level !== Level.NONE, not
!!level. Numeric enum value 0 is falsy.
- Never mix numeric and string members in the same enum.
- Use enums over unions when you need a runtime object (iteration, lookup), a
namespace for related constants, or reverse mapping.
Type Assertions
- Prefer annotations over assertions.
: Foocatches errors;as Foohides them. - Always use `as` syntax, never angle brackets (
<Foo>value) — angle brackets
conflict with JSX.
- Assertions are justified when you genuinely know more than the compiler: values
from JSON.parse, DOM API returning wider types, trusted external sources.
- Double assertions through `unknown`:
value as unknown as Foo. Never useany
as the intermediate type.
Overloads
- Prefer union types over overloads when parameter types differ but logic is shared.
- Prefer optional parameters over overloads when signatures differ only in trailing
params.
- When overloads are necessary, put specific signatures before general ones —
TypeScript picks the first matching overload.
Class Patterns
- Omit `public` — it's the default. Only use
publicon non-readonly constructor
parameter properties.
- Use `private` for internal state,
protectedfor subclass access. - Use `readonly` on properties never reassigned after construction.
- Use constructor parameter properties to avoid boilerplate:
constructor(private readonly db: Database) {}.
- Initialize fields where declared when possible:
private count = 0. - Require `override` keyword on overridden methods (
noImplicitOverride).
Callback Types
- Use `void` return for callbacks whose return value is ignored.
- Don't use optional parameters in callbacks — callers can always ignore extra args.
(data: unknown, elapsed: number) => void not (data: unknown, elapsed?: number) => any.
Branded Types
Use branded types for nominal-like safety when structural typing is too permissive: domain IDs (UserId vs OrderId), validated strings (Email), units (Meters vs Kilometers). Pattern: type UserId = string & { readonly __brand: unique symbol }. Keep the branding mechanism consistent across the project — unique symbol is most robust.
Array Type Syntax
Use T[] for simple element types (string[], User[]). Use Array<T> for complex element types (Array<string | number>). Same rule applies to readonly variants.
Configuration (tsconfig.json)
- `strict: true` always. Non-negotiable. Also enable
noUncheckedIndexedAccess
and noImplicitOverride.
- Module resolution:
module: "NodeNext"when transpiling with tsc;
module: "preserve" with external bundlers (Vite, esbuild, Bun). Use verbatimModuleSyntax: true in both cases.
- Target:
es2022(stable). Setlibto includedomfor browser projects. - Never `@ts-ignore`. Use
@ts-expect-errorin tests only, with a comment.
Never @ts-nocheck in production.
- Keep `tsconfig.json` minimal. Use
extendsfor shared configs. Separate
tsconfig.build.json for builds (excludes tests, scripts).
See ${CLAUDE_SKILL_DIR}/references/configuration.md for the full options catalog, library project setup, and project structure guidance.
Application
When writing TypeScript code:
- Apply all conventions silently — don't narrate each rule being followed.
- Match the project's existing patterns (interface vs type preference, enum style).
- If an existing codebase contradicts a convention, follow the codebase and flag the
divergence once.
When reviewing TypeScript code:
- Cite the specific issue and show the fix inline.
- Don't lecture — state what's wrong and how to fix it.
Integration
The javascript skill is a hard prerequisite. The JavaScript skill governs code patterns; this skill governs type-level choices.
{
"sources": {
"TS Handbook - Narrowing & Type Guards": "https://raw.githubusercontent.com/microsoft/TypeScript-Website/v2/packages/documentation/copy/en/handbook-v2/Narrowing.md",
"TS Handbook - Generics": "https://raw.githubusercontent.com/microsoft/TypeScript-Website/v2/packages/documentation/copy/en/handbook-v2/Type%20Manipulation/Generics.md",
"TS Handbook - Conditional Types": "https://raw.githubusercontent.com/microsoft/TypeScript-Website/v2/packages/documentation/copy/en/handbook-v2/Type%20Manipulation/Conditional%20Types.md",
"TS Handbook - Mapped Types": "https://raw.githubusercontent.com/microsoft/TypeScript-Website/v2/packages/documentation/copy/en/handbook-v2/Type%20Manipulation/Mapped%20Types.md",
"TS Handbook - Template Literal Types": "https://raw.githubusercontent.com/microsoft/TypeScript-Website/v2/packages/documentation/copy/en/handbook-v2/Type%20Manipulation/Template%20Literal%20Types.md",
"TS Handbook - Utility Types": "https://raw.githubusercontent.com/microsoft/TypeScript-Website/v2/packages/documentation/copy/en/reference/Utility%20Types.md",
"TS Handbook - Do's and Don'ts": "https://raw.githubusercontent.com/microsoft/TypeScript-Website/v2/packages/documentation/copy/en/declaration-files/Do's%20and%20Don'ts.md",
"Google TypeScript Style Guide": "https://google.github.io/styleguide/tsguide.html",
"Total TypeScript - TSConfig Cheat Sheet": "https://www.totaltypescript.com/tsconfig-cheat-sheet",
"typescript-eslint Rules Overview": "https://typescript-eslint.io/rules/",
"typescript-eslint Shared Configs": "https://typescript-eslint.io/users/configs/"
},
"lastFetched": "2026-02-16T13:19:39.900Z"
}
TypeScript Configuration
tsconfig.json Essentials
Base Options (All Projects)
{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true
}
}| Option | Why |
|---|---|
esModuleInterop | Fixes CJS/ESM interop issues |
skipLibCheck | Skips .d.ts checking for performance |
target: "es2022" | Stable target; prefer over esnext |
allowJs | Allows .js imports in TS projects |
resolveJsonModule | Enables JSON imports with type safety |
moduleDetection: "force" | Treats all files as modules (avoids block-scope errors) |
isolatedModules | Prevents features unsafe in single-file transpilation |
verbatimModuleSyntax | Forces import type/export type for type-only imports |
Strictness (All Projects)
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true
}
}| Option | Why |
|---|---|
strict | Enables all strict checks. Non-negotiable. |
noUncheckedIndexedAccess | Array/object index access returns `T \ |
noImplicitOverride | Requires override keyword on overridden methods |
Optional strictness (add per project preference):
noImplicitReturns— all code paths must returnnoFallthroughCasesInSwitch— prevent switch fallthroughnoUnusedLocals/noUnusedParameters— flag unused code (can be noisy)
Module System
Transpiling with `tsc` (Node.js):
{
"compilerOptions": {
"module": "NodeNext",
"outDir": "dist",
"sourceMap": true
}
}module: "NodeNext" implies moduleResolution: "NodeNext" — supports both ESM and CJS based on package.json "type" field.
Using an external bundler (Vite, esbuild, webpack, Bun):
{
"compilerOptions": {
"module": "preserve",
"noEmit": true
}
}module: "preserve" implies moduleResolution: "Bundler" — lets the bundler handle module resolution while TS focuses on type checking.
Library Projects
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}For monorepo libraries, also add "composite": true to enable project references and incremental builds.
Runtime Environment
DOM (browser):
{ "compilerOptions": { "lib": ["es2022", "dom", "dom.iterable"] } }Server-only (Node.js/Bun):
{ "compilerOptions": { "lib": ["es2022"] } }Import Conventions
Use import type for Types
import type { User } from "./types";
import { createUser } from "./users";
// Inline form:
import { type User, createUser } from "./users";verbatimModuleSyntax enforces this. Type imports are erased at compile time and produce no runtime code.
Use export type for Type Re-exports
export type { User } from "./types";Required for correct behavior with isolatedModules and file-by-file transpilation.
No namespace, No require
// Bad:
namespace Foo { ... }
import x = require("foo");
// Good:
export function foo() { ... }
import { foo } from "./foo";ES modules are the only supported module system. namespace is legacy.
Array Type Syntax
| Element type | Syntax | Example |
|---|---|---|
| Simple (alphanumeric) | T[] | string[], number[], User[] |
| Complex (union, object) | Array<T> | `Array<string \ |
| Readonly simple | readonly T[] | readonly string[] |
| Readonly complex | ReadonlyArray<T> | `ReadonlyArray<string \ |
| Nested simple | T[][] | string[][] |
Compiler Directives
@ts-ignore and @ts-expect-error
Do not use `@ts-ignore`. It suppresses all errors on the next line, making future type errors invisible.
`@ts-expect-error` is acceptable in tests when deliberately testing invalid usage. It errors when the suppressed line has no error, so it won't silently mask changes.
// Bad: hides all errors forever
// @ts-ignore
const x: string = 42;
// Acceptable in tests: documents expected failure
// @ts-expect-error — testing invalid input handling
const result = processString(42);Prefer narrowing or explicit casts over suppression. If you must suppress, use @ts-expect-error with a comment explaining why.
@ts-nocheck
Never use @ts-nocheck in production code. It disables all type checking for the entire file.
Project Structure Tips
- Use `paths` sparingly. Prefer relative imports. Deep
../../../chains
suggest the module structure needs refactoring, not aliases.
- Keep `tsconfig.json` minimal. Use
extendsfor shared base configs. - `include` explicitly. Don't rely on defaults — specify which directories
to compile.
- Separate `tsconfig.build.json` for builds (excludes tests, scripts).
Generics and Type-Level Programming
Generics allow components to work over a variety of types while preserving type safety. Use them to write reusable, type-safe abstractions — but keep them as simple as possible.
Generic Functions
function identity<T>(arg: T): T {
return arg;
}
// Inference works — don't specify the type argument when it's obvious:
const result = identity("hello"); // result: stringGeneric Constraints
Constrain type parameters with extends to access specific properties:
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength("hello"); // OK: string has length
logLength([1, 2, 3]); // OK: array has length
logLength(42); // Error: number has no lengthUsing Type Parameters in Constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = { name: "Alice", age: 30 };
getProperty(person, "name"); // OK: "name" is keyof typeof person
getProperty(person, "foo"); // Error: "foo" is not a keyGeneric Parameter Defaults
interface Container<T, U = T[]> {
element: T;
children: U;
}
// U defaults to T[] when not specified:
const c: Container<string> = {
element: "hello",
children: ["world"],
};Utility Types
TypeScript's standard library includes essential type operators:
Object Transformation
| Type | Purpose |
|---|---|
Partial<T> | All properties optional |
Required<T> | All properties required |
Readonly<T> | All properties readonly |
Pick<T, Keys> | Subset of properties |
Omit<T, Keys> | All except specified properties |
Record<Keys, T> | Object with specified keys and value type |
interface User {
name: string;
email: string;
age: number;
}
type UserUpdate = Partial<User>;
// { name?: string; email?: string; age?: number }
type UserPreview = Pick<User, "name" | "email">;
// { name: string; email: string }
type UserWithoutAge = Omit<User, "age">;
// { name: string; email: string }Union Manipulation
| Type | Purpose |
|---|---|
Exclude<Union, Excluded> | Remove members from union |
Extract<Union, Extracted> | Keep only matching members |
NonNullable<T> | Remove null and undefined |
type T = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type U = NonNullable<string | null>; // stringFunction Types
| Type | Purpose |
|---|---|
ReturnType<T> | Extract return type |
Parameters<T> | Extract parameter types as tuple |
Awaited<T> | Unwrap Promise types recursively |
NoInfer<T> | Block inference at this position |
function fetchUser(id: string): Promise<User> { ... }
type FetchReturn = ReturnType<typeof fetchUser>; // Promise<User>
type FetchParams = Parameters<typeof fetchUser>; // [id: string]
type ResolvedUser = Awaited<ReturnType<typeof fetchUser>>; // UserNoInfer<T> (TS 5.4+)
Prevents a parameter from being used as an inference site:
function createConfig<C extends string>(
colors: C[],
defaultColor?: NoInfer<C>,
) { ... }
createConfig(["red", "green"], "red"); // OK
createConfig(["red", "green"], "blue"); // ErrorConditional Types
Types that act like if-statements in the type system:
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // falseinfer Keyword
Extract types from within conditional checks:
type ElementType<T> = T extends Array<infer E> ? E : T;
type A = ElementType<string[]>; // string
type B = ElementType<number>; // number
type UnwrapReturn<T> = T extends (...args: never[]) => infer R ? R : never;Distributive Behavior
Conditional types distribute over unions:
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[] (not (string | number)[])Wrap in [T] to prevent distribution:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result = ToArrayNonDist<string | number>;
// (string | number)[]Mapped Types
Transform all properties of a type:
type Optional<T> = {
[P in keyof T]?: T[P];
};
// Remove readonly:
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
// Remove optional:
type Concrete<T> = {
[P in keyof T]-?: T[P];
};Key Remapping with as
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface Person { name: string; age: number }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }Filter keys by producing never:
type RemoveKind<T> = {
[P in keyof T as Exclude<P, "kind">]: T[P];
};Template Literal Types
String manipulation at the type level:
type EventName<T extends string> = `${T}Changed`;
type Result = EventName<"name" | "age">;
// "nameChanged" | "ageChanged"Intrinsic string types: Uppercase, Lowercase, Capitalize, Uncapitalize.
Complexity Budget
Type-level programming is powerful but has real costs:
- Every complex type adds cognitive load for all readers
- IDE performance degrades with deeply nested conditional/mapped types
- Error messages become cryptic when types are too abstract
Rule of thumb: If you can't explain what a type does in one sentence, it's too complex. Split it, simplify it, or use explicit interfaces instead.
Complexity Tiers
| Tier | Tools | Use When |
|---|---|---|
| Simple | interface, type alias, union | Always — default choice |
| Moderate | Partial, Pick, Omit, Record | Well-known transformations |
| Advanced | Conditional, mapped, template literal | Library code, framework types |
| Expert | Recursive types, complex infer chains | Rarely — last resort |
Stay at the lowest tier that solves your problem.
Best Practices
- Use the simplest construct that works. Interface extension over
Pick.
Explicit properties over mapped types. Repetition is cheaper than complexity.
- Mapped and conditional types are powerful but costly — they hurt readability,
IDE performance, and refactoring. Use only when the alternative is worse.
- Avoid return-type-only generics. If a generic parameter appears only in the
return type, it cannot be inferred and forces callers to guess.
- Prefer built-in utility types over hand-rolling equivalents.
- Keep generic constraints tight —
<T extends Record<string, unknown>>is
better than <T extends object> when you need string keys.
When to Use Utility Types vs Explicit Interfaces
Prefer explicit interfaces when:
- The type represents a distinct domain concept
- The shape is referenced in many places
- Readability matters more than DRY
Use utility types when:
- Deriving a type from an existing one is unambiguous
- The transformation is a well-known pattern (e.g., update payload =
Partial<T>) - The source type is the canonical definition
// Good: Partial communicates intent clearly
function updateUser(id: string, changes: Partial<User>): void {}
// Bad: Pick where a purpose-named interface is clearer
type LoginInfo = Pick<User, "email" | "passwordHash" | "lastLogin" | "mfaEnabled">;
// Better: explicit interface with a meaningful name
interface LoginInfo {
email: string;
passwordHash: string;
lastLogin: Date;
mfaEnabled: boolean;
}Narrowing and Type Guards
Narrowing is TypeScript's ability to refine types within conditional branches. Write code that narrows naturally — the compiler follows your control flow.
Built-in Narrowing Constructs
typeof Guards
TypeScript understands typeof checks and narrows accordingly:
function process(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase(); // value: string
}
return value.toFixed(2); // value: number
}typeof returns: "string", "number", "bigint", "boolean", "symbol", "undefined", "object", "function".
Pitfall: typeof null === "object". Always check for null separately.
instanceof Guards
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString()); // x: Date
} else {
console.log(x.toUpperCase()); // x: string
}
}in Operator
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim(); // animal: Fish
}
return animal.fly(); // animal: Bird
}Note: Optional properties appear in both branches. If Human has swim?, "swim" in animal will narrow to Fish | Human, not just Fish.
Equality Narrowing
function example(x: string | number, y: string | boolean) {
if (x === y) {
// x and y are both string (the only common type)
x.toUpperCase();
}
}== null checks both null and undefined — this is the one valid use of ==:
function handle(value: string | null | undefined) {
if (value != null) {
// value: string (neither null nor undefined)
}
}Truthiness Narrowing
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") {
for (const s of strs) { console.log(s); }
} else if (typeof strs === "string") {
console.log(strs);
}
}Pitfall: Truthiness checks fail on "", 0, NaN, false. Prefer explicit null checks over truthiness when these values are valid.
Discriminated Unions
The most important narrowing pattern. Add a literal kind field to each variant:
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
}
}Exhaustiveness Checking
Add a default branch that assigns to never — the compiler will error if you add a new variant without handling it:
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustive: never = shape;
return _exhaustive;
}
}If you add Triangle to Shape without a case, TypeScript errors: Type 'Triangle' is not assignable to type 'never'.
User-Defined Type Guards (Type Predicates)
Define reusable guards with param is Type return annotation:
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
// Use in conditionals
if (isFish(pet)) {
pet.swim(); // pet: Fish
} else {
pet.fly(); // pet: Bird
}
// Use with filter
const fishes: Fish[] = zoo.filter(isFish);Assertion Functions
function assertIsError(value: unknown): asserts value is Error {
if (!(value instanceof Error)) {
throw new Error("Expected an Error instance");
}
}
try {
doSomething();
} catch (e: unknown) {
assertIsError(e);
console.log(e.message); // e: Error
}Control Flow Analysis
TypeScript tracks types across assignments and early returns:
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
}
// TypeScript knows padding is string here (number case returned)
return padding + input;
}Rules Summary
- Prefer discriminated unions over optional properties for variant types
- Use exhaustive switches with
neverdefault to catch missing cases - Use `!= null` (loose equality) to check both null and undefined
- Write type predicates for reusable, complex guards
- Avoid non-null assertions (`!`) — narrow instead
- Use assertion functions (
asserts x is T) for validation at boundaries
TypeScript Patterns and Declarations
Interfaces vs Type Aliases
When to Use interface
Use interface for object shapes — it has better error messages, IDE support, and compiler performance:
interface User {
name: string;
email: string;
age: number;
}
// Extension is explicit and readable:
interface Admin extends User {
permissions: string[];
}When to Use type
Use type for everything that isn't an object shape:
// Unions:
type Status = "active" | "inactive" | "pending";
// Intersections:
type WithTimestamps = User & { createdAt: Date; updatedAt: Date };
// Tuples:
type Coordinate = [number, number];
// Function types:
type Handler = (event: Event) => void;
// Mapped/conditional types:
type Optional<T> = { [K in keyof T]?: T[K] };Decision Rule
Object shape with known properties? interface. Everything else? type.
Do not mix arbitrarily — pick one pattern per kind and stay consistent within a project.
Type Assertions
Prefer Annotations Over Assertions
// Bad: assertion hides missing/extra property errors
const user = { name: "Alice", emal: "a@b.com" } as User;
// Good: annotation catches the typo immediately
const user: User = { name: "Alice", emal: "a@b.com" };
// Error: 'emal' does not exist on type 'User'When Assertions Are Justified
Sometimes you genuinely know more than the compiler:
// OK: value comes from trusted external source
const config = JSON.parse(rawConfig) as AppConfig;
// OK: DOM API returns wider type
const input = document.getElementById("email") as HTMLInputElement;Always use as syntax, never angle brackets:
// Bad:
const x = (<Foo>value).method();
// Good:
const x = (value as Foo).method();Double Assertions
When TypeScript rejects a direct assertion, cast through unknown:
// value is Foo because [specific reason]
const result = (value as unknown as Foo).method();Use unknown as the intermediate type, never any.
Null Handling
Prefer Optional (?) Over | undefined
// Good: optional field
interface Config {
debug?: boolean;
timeout?: number;
}
// Good: optional parameter
function connect(host: string, port?: number) { ... }
// Avoid: explicit undefined in the type
interface Config {
debug: boolean | undefined; // Forces callers to pass undefined explicitly
}Nullable Type Aliases
Do not include null or undefined in type aliases:
// Bad: null baked into the alias
type MaybeUser = User | null;
// Good: nullability at the use site
function getUser(id: string): User | null { ... }This keeps nullability visible where it matters and prevents it from spreading through the codebase.
Null Narrowing
// Loose equality checks both null and undefined:
if (value != null) {
// value is neither null nor undefined
}
// Optional chaining for access:
const name = user?.profile?.name;
// Nullish coalescing for defaults:
const timeout = config.timeout ?? 3000;Enums
Prefer String Enums
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}String enums have meaningful runtime values, predictable behavior, and readable debug output.
Prefer Union Types for Simple Cases
// Simpler than an enum:
type Direction = "up" | "down" | "left" | "right";
// Use enum when you need:
// - Runtime object (iteration, lookup)
// - Namespace for related constants
// - Reverse mapping (numeric enums only)Enum Anti-Patterns
// Bad: numeric enums with implicit values
enum Status { Active, Inactive } // Active = 0, Inactive = 1
// Bad: boolean coercion of enums
if (status) { ... } // Active (0) is falsy!
// Good: explicit comparison
if (status !== Status.Inactive) { ... }
// Bad: mixed numeric and string members
enum Mixed { A = 0, B = "b" } // Never do thisClass Patterns
Visibility
- Omit
public— it's the default. Only usepublicon non-readonly
constructor parameter properties.
- Use
privatefor internal state,protectedfor subclass access. - Use
readonlyon properties that are never reassigned after construction.
class UserService {
// No "public" needed — it's the default
readonly name: string;
constructor(
private readonly db: Database,
public apiKey: string, // public needed for parameter property
) {
this.name = "UserService";
}
}Parameter Properties
Use constructor parameter properties to avoid boilerplate:
// Bad: manual assignment
class Service {
private readonly db: Database;
constructor(db: Database) {
this.db = db;
}
}
// Good: parameter property
class Service {
constructor(private readonly db: Database) {}
}Field Initialization
Initialize where declared when possible:
class Counter {
private count = 0; // No constructor needed
private readonly items: string[] = [];
}Structural Typing
TypeScript is structural, not nominal. A value matches a type if it has the required properties — the name of the type doesn't matter.
Annotate for Precision
interface Animal { sound: string; name: string }
// Bad: inferred type, error shows at call site far away
const cat = { sound: "meow" };
makeSound(cat); // Error here, but the bug is above
// Good: annotated type, error shows at declaration
const horse: Animal = { sound: "neigh" };
// Error: Property 'name' is missingUse Structural Types, Not Classes
When defining data shapes, use interfaces:
// Bad: class used as data shape
class Foo {
readonly a: number;
readonly b: string;
}
// Good: interface for structure
interface Foo {
a: number;
b: string;
}Overload Patterns
Prefer Union Types Over Overloads
// Bad: separate overloads for each type
function format(x: string): string;
function format(x: number): string;
function format(x: string | number): string { ... }
// Good: union parameter
function format(x: string | number): string { ... }Prefer Optional Parameters Over Overloads
// Bad: overloads differing in trailing params
interface Example {
diff(one: string): number;
diff(one: string, two: string): number;
}
// Good: optional parameter
interface Example {
diff(one: string, two?: string): number;
}Overload Ordering
When overloads are necessary, put specific signatures before general ones:
function handle(x: HTMLDivElement): string;
function handle(x: HTMLElement): number;
function handle(x: unknown): unknown;
function handle(x: unknown): unknown { ... }TypeScript picks the first matching overload. General-before-specific hides the specific overloads.
Branded Types
TypeScript's structural typing means any string is assignable to any other string-typed variable. Branded types add nominal-like safety:
type UserId = string & { readonly __brand: unique symbol };
type OrderId = string & { readonly __brand: unique symbol };
function createUserId(id: string): UserId {
return id as UserId;
}
function getUser(id: UserId): User { ... }
const userId = createUserId("usr_123");
const orderId = "ord_456" as OrderId;
getUser(userId); // OK
getUser(orderId); // Error: OrderId is not assignable to UserId
getUser("raw"); // Error: string is not assignable to UserIdUse branded types when:
- Domain IDs should not be interchangeable (UserId vs OrderId)
- Validated strings need type-level tracking (Email, URL)
- Units must not be mixed (Meters vs Kilometers)
Keep the branding mechanism consistent across the project. The unique symbol pattern is the most robust — __brand: "UserId" also works but allows cross-assignment between brands with the same string.
unknown vs any
Decision Table
| Situation | Use |
|---|---|
| Value from external source (API, JSON.parse, user input) | unknown |
| Function accepts anything, passes through without touching | unknown |
| Migrating JS to TS incrementally | any (temporary, with comment) |
| Test mock that intentionally bypasses type checking | any (with comment) |
| You're too lazy to type it properly | Fix the types |
Using unknown Safely
unknown requires narrowing before use:
function processValue(value: unknown): string {
// Bad: value.toString() — Error, unknown has no methods
// Good: narrow first
if (typeof value === "string") return value;
if (typeof value === "number") return String(value);
if (value instanceof Error) return value.message;
return JSON.stringify(value);
}{} Type
{} means "any non-nullish value" — it's almost never what you want:
| Type | Allows |
|---|---|
unknown | Everything (null, undefined, primitives, objects) |
object | Non-primitive, non-null values |
{} | Any non-nullish value (primitives included) |
Record<string, unknown> | Objects with string keys |
Prefer unknown for opaque values, Record<string, unknown> for dict-like objects, object when you need "any non-primitive".
Callback Types
- Use
voidreturn for callbacks whose return value is ignored. - Don't use optional parameters in callbacks — callers can always ignore
extra arguments.
// Bad:
type Callback = (data: unknown, elapsed?: number) => any;
// Good:
type Callback = (data: unknown, elapsed: number) => void;