Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:22:35 +08:00
commit 6fcffca9b0
35 changed files with 8235 additions and 0 deletions

View File

@@ -0,0 +1,383 @@
---
name: using-type-guards
description: 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.
allowed-tools: Read, Write, Edit, Glob, Grep, Bash, Task, TodoWrite
version: 1.0.0
---
<role>
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.
</role>
<when-to-activate>
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
</when-to-activate>
<overview>
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
</overview>
<workflow>
## 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)
```typescript
typeof value === "string"
value instanceof Error
"property" in value
Array.isArray(value)
```
**Custom Type Predicates** (object shapes, complex logic)
```typescript
function isUser(value: unknown): value is User {
return typeof value === "object" &&
value !== null &&
"id" in value;
}
```
**Validation Libraries** (nested structures, multiple fields)
```typescript
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
</workflow>
<examples>
## Example 1: Built-in Type Guards
### typeof Guard
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
</examples>
<progressive-disclosure>
## 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
</progressive-disclosure>
<constraints>
**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
</constraints>
<patterns>
## 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
</patterns>
<validation>
## 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
</validation>
<testing-guards>
## 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.
</testing-guards>

View File

@@ -0,0 +1,97 @@
# Advanced Type Guard Patterns
This reference provides detailed examples of advanced type guard patterns.
## Pattern 1: Optional Property Guard
```typescript
interface Config {
apiKey: string;
timeout?: number;
}
function isConfig(value: unknown): value is Config {
if (typeof value !== "object" || value === null) {
return false;
}
const obj = value as Record<string, unknown>;
if (typeof obj.apiKey !== "string") {
return false;
}
if ("timeout" in obj && typeof obj.timeout !== "number") {
return false;
}
return true;
}
```
## Pattern 2: Array Element Guard
```typescript
function everyElementIs<T>(
arr: unknown[],
guard: (item: unknown) => item is T
): arr is T[] {
return arr.every(guard);
}
const data: unknown = ["a", "b", "c"];
if (Array.isArray(data) && everyElementIs(data, (item): item is string => typeof item === "string")) {
const lengths = data.map(str => str.length);
}
```
## Pattern 3: Tuple Guard
```typescript
function isTuple<T, U>(
value: unknown,
guard1: (item: unknown) => item is T,
guard2: (item: unknown) => item is U
): value is [T, U] {
return (
Array.isArray(value) &&
value.length === 2 &&
guard1(value[0]) &&
guard2(value[1])
);
}
const isStringNumberPair = (value: unknown): value is [string, number] =>
isTuple(
value,
(item): item is string => typeof item === "string",
(item): item is number => typeof item === "number"
);
```
## Pattern 4: Record Guard
```typescript
function isStringRecord(value: unknown): value is Record<string, string> {
if (typeof value !== "object" || value === null) {
return false;
}
return Object.values(value).every(val => typeof val === "string");
}
```
## Pattern 5: Enum Guard
```typescript
enum Status {
Active = "active",
Inactive = "inactive",
Pending = "pending"
}
function isStatus(value: unknown): value is Status {
return Object.values(Status).includes(value as Status);
}
```

View File

@@ -0,0 +1,110 @@
# Nested Type Guard Validation
Examples of validating complex nested object structures.
## Basic Nested Validation
```typescript
interface Address {
street: string;
city: string;
zipCode: string;
}
interface UserWithAddress {
id: string;
name: string;
address: Address;
}
function isAddress(value: unknown): value is Address {
return (
typeof value === "object" &&
value !== null &&
"street" in value &&
"city" in value &&
"zipCode" in value &&
typeof (value as Address).street === "string" &&
typeof (value as Address).city === "string" &&
typeof (value as Address).zipCode === "string"
);
}
function isUserWithAddress(value: unknown): value is UserWithAddress {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
"address" in value &&
typeof (value as UserWithAddress).id === "string" &&
typeof (value as UserWithAddress).name === "string" &&
isAddress((value as UserWithAddress).address)
);
}
```
## Composable Guards Pattern
Build complex guards by composing simpler ones:
```typescript
function hasStringProperty(obj: unknown, key: string): boolean {
return (
typeof obj === "object" &&
obj !== null &&
key in obj &&
typeof (obj as Record<string, unknown>)[key] === "string"
);
}
function isUserWithAddress(value: unknown): value is UserWithAddress {
return (
hasStringProperty(value, "id") &&
hasStringProperty(value, "name") &&
typeof value === "object" &&
value !== null &&
"address" in value &&
isAddress((value as UserWithAddress).address)
);
}
```
## Deep Nesting Example
```typescript
interface Company {
name: string;
address: Address;
}
interface Employee {
id: string;
name: string;
company: Company;
}
function isCompany(value: unknown): value is Company {
return (
typeof value === "object" &&
value !== null &&
"name" in value &&
"address" in value &&
typeof (value as Company).name === "string" &&
isAddress((value as Company).address)
);
}
function isEmployee(value: unknown): value is Employee {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
"company" in value &&
typeof (value as Employee).id === "string" &&
typeof (value as Employee).name === "string" &&
isCompany((value as Employee).company)
);
}
```

View File

@@ -0,0 +1,97 @@
# Testing Type Guards
Complete guide to unit testing type guards with comprehensive test cases.
## Testing Strategy
When testing type guards, ensure coverage for:
1. Valid inputs (happy path)
2. Missing required properties
3. Wrong property types
4. Null and undefined
5. Non-object primitives
6. Edge cases specific to your type
## Complete Test Suite Example
```typescript
describe("isUser", () => {
it("returns true for valid user", () => {
expect(isUser({ id: "1", name: "Alice", email: "alice@example.com" })).toBe(true);
});
it("returns false for missing property", () => {
expect(isUser({ id: "1", name: "Alice" })).toBe(false);
});
it("returns false for wrong property type", () => {
expect(isUser({ id: 1, name: "Alice", email: "alice@example.com" })).toBe(false);
});
it("returns false for null", () => {
expect(isUser(null)).toBe(false);
});
it("returns false for undefined", () => {
expect(isUser(undefined)).toBe(false);
});
it("returns false for non-object", () => {
expect(isUser("not an object")).toBe(false);
});
});
```
## Testing Array Guards
```typescript
describe("isStringArray", () => {
it("returns true for array of strings", () => {
expect(isStringArray(["a", "b", "c"])).toBe(true);
});
it("returns true for empty array", () => {
expect(isStringArray([])).toBe(true);
});
it("returns false for mixed types", () => {
expect(isStringArray(["a", 1, "c"])).toBe(false);
});
it("returns false for non-array", () => {
expect(isStringArray("not array")).toBe(false);
});
});
```
## Testing Assertion Functions
```typescript
describe("assertIsUser", () => {
it("does not throw for valid user", () => {
expect(() => assertIsUser({ id: "1", name: "Alice", email: "alice@example.com" })).not.toThrow();
});
it("throws for invalid user", () => {
expect(() => assertIsUser({ id: "1" })).toThrow("Invalid user data");
});
it("throws for null", () => {
expect(() => assertIsUser(null)).toThrow();
});
});
```
## Test Coverage Checklist
- [ ] Valid inputs
- [ ] Each required property missing
- [ ] Each property with wrong type
- [ ] Null input
- [ ] Undefined input
- [ ] Non-object primitives
- [ ] Empty objects/arrays
- [ ] Nested object validation
- [ ] Optional properties (present and absent)
- [ ] Edge cases for your domain