Files
2025-11-29 18:22:35 +08:00

9.5 KiB

name, description, allowed-tools, version
name description allowed-tools version
using-type-guards Teaches how to write custom type guards with type predicates and use built-in type narrowing in TypeScript. Use when working with unknown types, union types, validating external data, or implementing type-safe runtime checks. Read, Write, Edit, Glob, Grep, Bash, Task, TodoWrite 1.0.0
This skill teaches how to safely narrow types at runtime using type guards, enabling type-safe handling of dynamic data and union types. Critical for validating external data and preventing runtime errors. This skill activates when:
  • Working with unknown or any types
  • Handling union types that need discrimination
  • Validating external data (APIs, user input, JSON)
  • Implementing runtime type checks
  • User mentions type guards, type narrowing, type predicates, or runtime validation
Type guards are runtime checks that inform TypeScript's type system about the actual type of a value. They bridge the gap between compile-time types and runtime values.

Three Categories:

  1. Built-in Guards: typeof, instanceof, in, Array.isArray()
  2. Type Predicates: Custom functions returning value is Type
  3. Assertion Functions: Functions that throw if type check fails

Key Pattern: Runtime check → Type narrowing → Safe access

## Type Guard Selection Flow

Step 1: Identify the Type Challenge

What do you need to narrow?

  • Primitive types → Use typeof
  • Class instances → Use instanceof
  • Object shapes → Use in or custom type predicate
  • Array types → Use Array.isArray() + element checks
  • Complex structures → Use validation library (Zod, io-ts)

Step 2: Choose Guard Strategy

Built-in Guards (primitives, classes, simple checks)

typeof value === "string"
value instanceof Error
"property" in value
Array.isArray(value)

Custom Type Predicates (object shapes, complex logic)

function isUser(value: unknown): value is User {
  return typeof value === "object" &&
         value !== null &&
         "id" in value;
}

Validation Libraries (nested structures, multiple fields)

const UserSchema = z.object({
  id: z.string(),
  email: z.string().email()
});

Step 3: Implement Guard

  1. Check for null and undefined first
  2. Check base type (object, array, primitive)
  3. Check structure (properties exist)
  4. Check property types
  5. Return type predicate or throw
## Example 1: Built-in Type Guards

typeof Guard

function processValue(value: unknown): string {
  if (typeof value === "string") {
    return value.toUpperCase();
  }

  if (typeof value === "number") {
    return value.toFixed(2);
  }

  if (typeof value === "boolean") {
    return value ? "yes" : "no";
  }

  throw new Error("Unsupported type");
}

typeof Guards Work For:

  • "string", "number", "boolean", "symbol", "undefined"
  • "function", "bigint"
  • "object" (but also matches null - check for null first!)

instanceof Guard

function handleError(error: unknown): string {
  if (error instanceof Error) {
    return error.message;
  }

  if (error instanceof CustomError) {
    return `Custom error: ${error.code}`;
  }

  return String(error);
}

instanceof Works For:

  • Class instances
  • Built-in classes (Error, Date, Map, Set, etc.)
  • DOM elements (HTMLElement, etc.)

in Guard

type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; sideLength: number };
type Shape = Circle | Square;

function getArea(shape: Shape): number {
  if ("radius" in shape) {
    return Math.PI * shape.radius ** 2;
  } else {
    return shape.sideLength ** 2;
  }
}

in Guard Best For:

  • Discriminating union types
  • Checking optional properties
  • Object shape validation

Example 2: Custom Type Predicates

Basic Type Predicate

interface User {
  id: string;
  name: string;
  email: string;
}

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "name" in value &&
    "email" in value &&
    typeof (value as User).id === "string" &&
    typeof (value as User).name === "string" &&
    typeof (value as User).email === "string"
  );
}

function processUser(data: unknown): void {
  if (isUser(data)) {
    console.log(data.name);
  } else {
    throw new Error("Invalid user data");
  }
}

Array Type Predicate

function isStringArray(value: unknown): value is string[] {
  return Array.isArray(value) && value.every(item => typeof item === "string");
}

function processNames(data: unknown): string {
  if (isStringArray(data)) {
    return data.join(", ");
  }
  throw new Error("Expected array of strings");
}

Nested Type Predicate

For complex nested structures, compose guards from simpler guards. See references/nested-validation.md for detailed examples.


Example 3: Discriminated Unions

Using Literal Type Discrimination

type Success = {
  status: "success";
  data: string;
};

type Failure = {
  status: "error";
  error: string;
};

type Result = Success | Failure;

function handleResult(result: Result): void {
  if (result.status === "success") {
    console.log("Data:", result.data);
  } else {
    console.error("Error:", result.error);
  }
}

Runtime Validation of Discriminated Union

function isSuccess(value: unknown): value is Success {
  return (
    typeof value === "object" &&
    value !== null &&
    "status" in value &&
    value.status === "success" &&
    "data" in value &&
    typeof (value as Success).data === "string"
  );
}

function isFailure(value: unknown): value is Failure {
  return (
    typeof value === "object" &&
    value !== null &&
    "status" in value &&
    value.status === "error" &&
    "error" in value &&
    typeof (value as Failure).error === "string"
  );
}

function isResult(value: unknown): value is Result {
  return isSuccess(value) || isFailure(value);
}

Example 4: Assertion Functions

function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new Error(`Expected string, got ${typeof value}`);
  }
}

function assertIsUser(value: unknown): asserts value is User {
  if (!isUser(value)) {
    throw new Error("Invalid user data");
  }
}

function processData(data: unknown): void {
  assertIsUser(data);

  console.log(data.name);
}

Assertion Functions:

  • Throw error if check fails
  • Narrow type for remainder of scope
  • Use asserts value is Type return type
  • Good for precondition checks
## Reference Files

Local References (in references/ directory):

  • advanced-patterns.md - Optional properties, arrays, tuples, records, enums
  • nested-validation.md - Complex nested object validation patterns
  • testing-guide.md - Complete unit testing guide with examples

Related Skills:

  • Runtime Validation Libraries: Use the using-runtime-checks skill for Zod/io-ts patterns
  • Unknown Type Handling: Use the avoiding-any-types skill for when to use type guards
  • Error Type Guards: Error-specific type guard patterns can be found in error handling documentation
**MUST:**
  • Check for null and undefined before property access
  • Check object base type before using in operator
  • Use type predicates (value is Type) for reusable guards
  • Validate all required properties exist
  • Validate property types, not just existence

SHOULD:

  • Compose simple guards into complex guards
  • Use discriminated unions for known variants
  • Prefer built-in guards over custom when possible
  • Name guard functions isType or assertIsType

NEVER:

  • Access properties before type guard
  • Forget to check for null (typeof null === "object"!)
  • Use type assertions instead of type guards for external data
  • Assume property exists after checking with in
  • Skip validating nested object types
## Common Patterns

Basic Patterns (covered in examples above):

  • Built-in guards: typeof, instanceof, in, Array.isArray()
  • Custom type predicates for object shapes
  • Discriminated union narrowing
  • Assertion functions

Advanced Patterns (see references/advanced-patterns.md):

  • Optional property validation
  • Array element guards with generics
  • Tuple guards
  • Record guards
  • Enum guards
## Type Guard Quality Checklist
  1. Null Safety:

    • Checks for null before using in or property access
    • Checks for undefined for optional values
  2. Complete Validation:

    • Validates all required properties exist
    • Validates property types, not just existence
    • Validates nested objects recursively
  3. Type Predicate:

    • Returns value is Type for reusable guards
    • Or uses asserts value is Type for assertion functions
  4. Edge Cases:

    • Handles arrays correctly
    • Handles empty objects
    • Handles inherited properties if relevant
  5. Composition:

    • Complex guards built from simpler guards
    • Guards are unit testable
## Unit Testing Type Guards

Test all edge cases: valid inputs, missing properties, wrong types, null, undefined, and non-objects.

See references/testing-guide.md for complete testing strategies and examples.