10 KiB
name, description, allowed-tools, version
| name | description | allowed-tools | version |
|---|---|---|---|
| validating-type-assertions | Teaches when type assertions are safe versus dangerous in TypeScript. Use when considering using 'as' keyword, type casting, or when working with external data that might use assertions instead of proper validation. | Read, Write, Edit, Glob, Grep, Bash, Task, TodoWrite | 1.0.0 |
- Code contains
askeyword or angle bracket syntax<Type> - Working with type assertions or type casting
- Converting between types
- User mentions type assertions, casting, or "as" keyword
- Code uses assertions on external data
- Type Guard: Runtime check that narrows types safely
- Type Assertion: Compile-time instruction telling TypeScript to trust you
Type assertions are TypeScript compiler directives, not runtime operations.
const data = JSON.parse(json) as User;
This compiles fine but provides ZERO runtime safety. If JSON is malformed, your code crashes.
Rule: Type assertions are safe only when YOU control the data or have already validated it.
## Assertion Safety Decision TreeQuestion 1: Where does this data come from?
- From your own code (constants, constructors) → Assertion OK
- From external source (API, user input, JSON) → NEVER assert, validate instead
Question 2: Have you validated the data?
- Yes, with runtime validation → Assertion OK after validation
- No validation → NEVER assert
Question 3: Is this a TypeScript limitation?
- Yes (const assertions, narrowing limitations) → Assertion OK
- No (trying to skip validation) → NEVER assert
❌ Asserting external API data
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json() as User;
return data;
}
Problem: If API returns different structure, runtime crash. TypeScript provides no protection.
✅ Validate instead
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data: unknown = await response.json();
if (!isUser(data)) {
throw new Error("Invalid user data from API");
}
return data;
}
❌ Asserting JSON.parse result
const config = JSON.parse(configString) as Config;
Problem: If JSON is malformed or wrong shape, crash at runtime.
✅ Validate with Zod
const data: unknown = JSON.parse(configString);
const config = ConfigSchema.parse(data);
❌ Asserting user input
function handleSubmit(formData: FormData) {
const user = {
name: formData.get("name"),
email: formData.get("email")
} as User;
saveUser(user);
}
Problem: FormData can contain anything. No validation.
✅ Validate form data
function handleSubmit(formData: FormData) {
const data = {
name: formData.get("name"),
email: formData.get("email")
};
const user = UserSchema.parse(data);
saveUser(user);
}
Example 2: Safe Assertions (OK to use)
✅ Const assertions
const routes = [
{ path: "/", component: "Home" },
{ path: "/about", component: "About" }
] as const;
type Route = typeof routes[number];
Safe because: Data is hardcoded, not external.
✅ After validation
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data: unknown = await response.json();
const result = UserSchema.safeParse(data);
if (!result.success) {
throw new Error("Invalid user data");
}
return result.data as User;
}
Safe because: Data validated before assertion. (Though result.data already has correct type, so assertion is redundant.)
✅ Constructor results
class User {
constructor(
public id: string,
public name: string
) {}
}
const users = [
new User("1", "Alice"),
new User("2", "Bob")
] as User[];
Safe because: You control construction, types are guaranteed.
✅ Type narrowing limitations
interface Circle { kind: "circle"; radius: number; }
interface Square { kind: "square"; size: number; }
type Shape = Circle | Square;
function getArea(shape: Shape): number {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
}
return shape.size ** 2;
}
Safe because: TypeScript narrows to Square after checking for circle. Use else to avoid assertion.
✅ Type widening prevention
const config = {
apiUrl: "https://api.example.com",
timeout: 5000
} as const;
Safe because: Preventing literal types from widening to general types.
✅ Unknown to specific after validation
function processError(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === "string") return error;
return String(error);
}
Safe because: Type guards narrow before use without assertions.
Example 3: Double Assertion Anti-Pattern
❌ Double assertion to bypass safety
const value = "not a number" as unknown as number;
Problem: Intentionally bypassing type system. Defeats TypeScript's purpose.
✅ Fix the types properly
const value: unknown = "not a number";
if (typeof value === "number") {
console.log(value.toFixed(2));
}
Example 4: Non-null Assertion
❌ Non-null assertion on external data
const user = await fetchUser(id);
console.log(user!.name);
Problem: If fetchUser can return null, this crashes.
✅ Check explicitly
const user = await fetchUser(id);
if (user) {
console.log(user.name);
} else {
console.log("User not found");
}
✅ Non-null assertion after explicit check
const element = document.getElementById("root");
if (!element) {
throw new Error("Root element not found");
}
element.appendChild(child);
Safe because: Checked for null and threw. TypeScript narrows automatically, no assertion needed.
Example 5: Assertion Functions vs Assertions
❌ Assertion to avoid validation
function getUser(data: unknown): User {
return data as User;
}
✅ Assertion function with validation
function assertIsUser(data: unknown): asserts data is User {
if (!isUser(data)) {
throw new Error("Invalid user data");
}
}
function getUser(data: unknown): User {
assertIsUser(data);
return data;
}
For related patterns:
- Runtime Validation: Use the using-runtime-checks skill for proper validation with Zod
- Type Guards: Use the using-type-guards skill for safe type narrowing
- Unknown Type: Use the avoiding-any-types skill for handling unknown data safely
- Validate external data with type guards or validation libraries
- Use assertion functions (
asserts value is Type) over direct assertions - Check for null/undefined before non-null assertions (
!) - Document WHY assertion is safe when used
SHOULD:
- Prefer type guards over assertions
- Use
as constfor literal type inference - Limit assertions to known-safe scenarios
- Consider if assertion indicates missing validation
NEVER:
- Assert on external data without validation (APIs, JSON, user input)
- Use double assertions (
as unknown as Type) - Use non-null assertion (
!) without prior check - Assert to silence compiler errors (fix the types instead)
- Trust that data "will be correct"
Type assertion is safe when ALL of these are true:
- Data source is internal/controlled (not external)
- OR data has been validated with runtime checks
- You understand what the assertion does (compiler directive, not runtime check)
- Assertion is documented with reason
- No double assertions being used
If ANY checkbox is false, use validation instead.
## Pattern LibraryPattern 1: Validated Assertion
function parseUser(data: unknown): User {
const result = UserSchema.safeParse(data);
if (!result.success) {
throw new ValidationError("Invalid user", result.error);
}
return result.data;
}
Pattern 2: Const Assertion for Config
const API_ENDPOINTS = {
users: "/api/users",
posts: "/api/posts",
comments: "/api/comments"
} as const;
type Endpoint = typeof API_ENDPOINTS[keyof typeof API_ENDPOINTS];
Pattern 3: Assertion Function
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
function handleShape(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
default:
assertNever(shape);
}
}
Pattern 4: Type Predicate Instead of Assertion
function isDefined<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
const values = [1, null, 2, undefined, 3];
const defined = values.filter(isDefined);
Find assertions: grep -rn " as " src/ and grep -rn "!" src/ | grep -v "!=="
Classify each: External data → add validation; After validation → verify; Const assertion → keep; Bypassing types → fix types
Replace pattern:
const data = JSON.parse(json) as User;
Becomes:
const data: unknown = JSON.parse(json);
const user = UserSchema.parse(data);
Enable strict mode: Set "strict": true and "noImplicitAny": true in tsconfig.json