Initial commit
This commit is contained in:
383
skills/using-type-guards/SKILL.md
Normal file
383
skills/using-type-guards/SKILL.md
Normal 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>
|
||||
97
skills/using-type-guards/references/advanced-patterns.md
Normal file
97
skills/using-type-guards/references/advanced-patterns.md
Normal 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);
|
||||
}
|
||||
```
|
||||
110
skills/using-type-guards/references/nested-validation.md
Normal file
110
skills/using-type-guards/references/nested-validation.md
Normal 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)
|
||||
);
|
||||
}
|
||||
```
|
||||
97
skills/using-type-guards/references/testing-guide.md
Normal file
97
skills/using-type-guards/references/testing-guide.md
Normal 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
|
||||
Reference in New Issue
Block a user