validating-type-assertions
- 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.
Question 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 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