Prerequisites
- Strong TypeScript fundamentals ๐
- Understanding of union types โก
- Basic knowledge of type system ๐ป
What you'll learn
- Use type assertions safely and correctly ๐ฏ
- Create custom type guards ๐๏ธ
- Narrow types with built-in guards ๐
- Handle unknown data confidently โจ
๐ฏ Introduction
Welcome to the defensive programming world of type assertions and type guards! ๐ In this guide, weโll explore how to handle uncertain types safely and tell TypeScript โtrust me, I know what Iโm doingโ - responsibly.
Think of type assertions as your โI know betterโ card ๐ด and type guards as your security checkpoints ๐ฌ. Together, they help you navigate the sometimes murky waters between what TypeScript thinks and what you know to be true.
By the end of this tutorial, youโll be narrowing types like a pro, handling any data that comes your way with confidence and safety! Letโs dive in! ๐โโ๏ธ
๐ Understanding Type Assertions and Type Guards
๐ค What Are Type Assertions?
Type assertions are like telling TypeScript โI know more than you doโ ๐ง. Theyโre a way to override TypeScriptโs inferred type when you have more information about the valueโs type.
Think of it as a security override ๐ - use with caution!
// TypeScript thinks it's unknown
const data: unknown = "Hello";
// You assert it's a string
const message = data as string;
๐ก What Are Type Guards?
Type guards are like bouncers at a club ๐ช - they check if something meets certain criteria before letting it through. They narrow down types within conditional blocks.
if (typeof value === "string") {
// TypeScript knows value is string here
console.log(value.toUpperCase());
}
๐ฏ Why Use Them?
Hereโs why these tools are essential:
- Handle External Data ๐: Safely process API responses
- Type Narrowing ๐ฏ: Eliminate union type ambiguity
- Runtime Safety ๐ก๏ธ: Validate types at runtime
- Better IntelliSense ๐ป: Help TypeScript help you
- Defensive Programming ๐ฐ: Guard against unexpected inputs
Real-world example: Processing user input from a form ๐ - you need to verify itโs the right type before using it!
๐ง Basic Syntax and Usage
๐ Type Assertions Basics
Letโs explore type assertions:
// ๐จ Two syntaxes for type assertions
// Angle bracket syntax (doesn't work in JSX)
const value1 = <string>someValue;
// 'as' syntax (works everywhere - preferred!)
const value2 = someValue as string;
// ๐ Real example
const element = document.getElementById('myButton');
// TypeScript thinks it might be null
const button = element as HTMLButtonElement;
button.disabled = true; // Now we can use button properties!
// ๐ฏ Asserting to more specific types
interface User {
id: number;
name: string;
role: "admin" | "user";
}
// API returns unknown data
const userData: unknown = {
id: 1,
name: "Alice",
role: "admin"
};
// Assert it's a User
const user = userData as User;
console.log(`๐ค ${user.name} is an ${user.role}`);
// โ ๏ธ Const assertions - super specific!
const config = {
endpoint: "https://api.example.com",
timeout: 5000
} as const;
// Now config.endpoint is "https://api.example.com", not string!
// ๐จ Type assertion with literals
const eventType = "click" as "click" | "focus" | "blur";
๐ฏ Type Guards Basics
Now letโs master type guards:
// ๐ก๏ธ Built-in type guards
// typeof guard
function processValue(value: string | number) {
if (typeof value === "string") {
// TypeScript knows it's string
console.log(`String length: ${value.length}`);
} else {
// TypeScript knows it's number
console.log(`Number squared: ${value ** 2}`);
}
}
// instanceof guard
class Cat {
meow() { console.log("๐ฑ Meow!"); }
}
class Dog {
bark() { console.log("๐ถ Woof!"); }
}
function petSound(pet: Cat | Dog) {
if (pet instanceof Cat) {
pet.meow();
} else {
pet.bark();
}
}
// ๐ฏ in operator guard
interface Bird {
fly(): void;
wings: number;
}
interface Fish {
swim(): void;
fins: number;
}
function move(animal: Bird | Fish) {
if ("wings" in animal) {
console.log(`๐ฆ
Flying with ${animal.wings} wings`);
animal.fly();
} else {
console.log(`๐ Swimming with ${animal.fins} fins`);
animal.swim();
}
}
// ๐ Custom type guards
function isString(value: unknown): value is string {
return typeof value === "string";
}
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
"role" in value
);
}
// Usage
const data: unknown = "Hello";
if (isString(data)) {
console.log(data.toUpperCase()); // TypeScript knows it's string!
}
๐ Combining Assertions and Guards
// ๐๏ธ Safe assertion pattern
function getElement<T extends HTMLElement>(
id: string,
type: new () => T
): T | null {
const element = document.getElementById(id);
if (element instanceof type) {
return element;
}
console.warn(`โ ๏ธ Element ${id} is not a ${type.name}`);
return null;
}
// Usage
const button = getElement("submit-btn", HTMLButtonElement);
if (button) {
button.onclick = () => console.log("๐ Clicked!");
}
// ๐จ Discriminated unions with guards
type Result<T> =
| { success: true; data: T }
| { success: false; error: Error };
function isSuccess<T>(result: Result<T>): result is { success: true; data: T } {
return result.success === true;
}
function processResult<T>(result: Result<T>) {
if (isSuccess(result)) {
console.log("โ
Data:", result.data);
} else {
console.log("โ Error:", result.error.message);
}
}
๐ก Practical Examples
๐ Example 1: API Response Handler
Letโs build a robust API response handler:
// ๐ API response types
interface ApiUser {
id: number;
name: string;
email: string;
role: "admin" | "user" | "guest";
metadata?: Record<string, any>;
}
interface ApiProduct {
id: string;
title: string;
price: number;
inStock: boolean;
categories: string[];
}
interface ApiError {
code: string;
message: string;
details?: unknown;
}
// ๐ก๏ธ Type guards for API responses
function isApiError(data: unknown): data is ApiError {
return (
typeof data === "object" &&
data !== null &&
"code" in data &&
"message" in data &&
typeof (data as ApiError).code === "string" &&
typeof (data as ApiError).message === "string"
);
}
function isApiUser(data: unknown): data is ApiUser {
if (typeof data !== "object" || data === null) return false;
const user = data as ApiUser;
return (
typeof user.id === "number" &&
typeof user.name === "string" &&
typeof user.email === "string" &&
["admin", "user", "guest"].includes(user.role)
);
}
function isApiProduct(data: unknown): data is ApiProduct {
if (typeof data !== "object" || data === null) return false;
const product = data as ApiProduct;
return (
typeof product.id === "string" &&
typeof product.title === "string" &&
typeof product.price === "number" &&
typeof product.inStock === "boolean" &&
Array.isArray(product.categories)
);
}
// ๐ฏ API client with type-safe responses
class ApiClient {
private baseUrl = "https://api.example.com";
async fetchUser(id: number): Promise<ApiUser> {
const response = await fetch(`${this.baseUrl}/users/${id}`);
const data: unknown = await response.json();
if (isApiError(data)) {
console.error(`โ API Error: ${data.message}`);
throw new Error(data.message);
}
if (!isApiUser(data)) {
console.error("โ ๏ธ Invalid user data received");
throw new Error("Invalid user data format");
}
console.log(`โ
User fetched: ${data.name}`);
return data;
}
async fetchProducts(): Promise<ApiProduct[]> {
const response = await fetch(`${this.baseUrl}/products`);
const data: unknown = await response.json();
if (!Array.isArray(data)) {
throw new Error("Expected array of products");
}
// Filter and validate each product
const validProducts = data.filter((item): item is ApiProduct => {
if (!isApiProduct(item)) {
console.warn("โ ๏ธ Skipping invalid product:", item);
return false;
}
return true;
});
console.log(`๐ฆ Fetched ${validProducts.length} valid products`);
return validProducts;
}
}
// ๐ฎ Usage
const api = new ApiClient();
// Fetch user with type safety
api.fetchUser(1)
.then(user => {
console.log(`๐ค ${user.name} (${user.role})`);
// TypeScript knows all user properties!
})
.catch(error => {
console.error("๐จ Failed to fetch user:", error);
});
๐จ Example 2: Form Validation System
Letโs create a type-safe form validator:
// ๐ Form field types
type FormValue = string | number | boolean | Date | File | null;
interface FormField {
name: string;
value: FormValue;
type: "text" | "number" | "email" | "date" | "file" | "checkbox";
required: boolean;
validators?: Array<(value: FormValue) => string | null>;
}
// ๐ Type guards for form values
function isNonEmpty(value: FormValue): value is string | number | Date | File {
return value !== null && value !== "" && value !== undefined;
}
function isValidEmail(value: unknown): boolean {
if (typeof value !== "string") return false;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value);
}
function isNumeric(value: unknown): value is number {
return typeof value === "number" && !isNaN(value);
}
function isDateValue(value: unknown): value is Date {
return value instanceof Date && !isNaN(value.getTime());
}
// ๐ฏ Form validator class
class FormValidator {
private errors = new Map<string, string[]>();
validateField(field: FormField): boolean {
const fieldErrors: string[] = [];
// Required validation
if (field.required && !isNonEmpty(field.value)) {
fieldErrors.push(`${field.name} is required`);
}
// Type-specific validation
switch (field.type) {
case "email":
if (isNonEmpty(field.value) && !isValidEmail(field.value)) {
fieldErrors.push("Invalid email format");
}
break;
case "number":
if (isNonEmpty(field.value) && !isNumeric(field.value)) {
fieldErrors.push("Must be a valid number");
}
break;
case "date":
if (isNonEmpty(field.value) && !isDateValue(field.value)) {
fieldErrors.push("Invalid date");
}
break;
case "file":
if (field.value && !(field.value instanceof File)) {
fieldErrors.push("Invalid file");
}
break;
}
// Custom validators
if (field.validators && isNonEmpty(field.value)) {
field.validators.forEach(validator => {
const error = validator(field.value);
if (error) fieldErrors.push(error);
});
}
// Store errors
if (fieldErrors.length > 0) {
this.errors.set(field.name, fieldErrors);
return false;
} else {
this.errors.delete(field.name);
return true;
}
}
validateForm(fields: FormField[]): boolean {
let isValid = true;
fields.forEach(field => {
if (!this.validateField(field)) {
isValid = false;
}
});
return isValid;
}
getErrors(): Record<string, string[]> {
return Object.fromEntries(this.errors);
}
displayErrors(): void {
if (this.errors.size === 0) {
console.log("โ
Form is valid!");
return;
}
console.log("โ Form validation errors:");
this.errors.forEach((errors, field) => {
console.log(`\n๐ ${field}:`);
errors.forEach(error => console.log(` - ${error}`));
});
}
}
// ๐ฎ Test the form validator
const validator = new FormValidator();
const formFields: FormField[] = [
{
name: "Email",
value: "invalid-email",
type: "email",
required: true
},
{
name: "Age",
value: "not a number" as any,
type: "number",
required: true
},
{
name: "Terms",
value: true,
type: "checkbox",
required: true,
validators: [
(value) => value === true ? null : "You must accept the terms"
]
},
{
name: "BirthDate",
value: new Date("2000-01-01"),
type: "date",
required: false,
validators: [
(value) => {
if (isDateValue(value)) {
const age = new Date().getFullYear() - value.getFullYear();
return age >= 18 ? null : "Must be 18 or older";
}
return null;
}
]
}
];
const isValid = validator.validateForm(formFields);
validator.displayErrors();
๐ฎ Example 3: Dynamic Component System
// ๐จ Component types
interface ButtonComponent {
type: "button";
text: string;
onClick: () => void;
variant?: "primary" | "secondary" | "danger";
}
interface InputComponent {
type: "input";
placeholder: string;
value: string;
onChange: (value: string) => void;
inputType?: "text" | "password" | "email";
}
interface SelectComponent {
type: "select";
options: Array<{ value: string; label: string }>;
value: string;
onChange: (value: string) => void;
}
type DynamicComponent = ButtonComponent | InputComponent | SelectComponent;
// ๐ก๏ธ Component type guards
function isButton(component: DynamicComponent): component is ButtonComponent {
return component.type === "button";
}
function isInput(component: DynamicComponent): component is InputComponent {
return component.type === "input";
}
function isSelect(component: DynamicComponent): component is SelectComponent {
return component.type === "select";
}
// ๐ฏ Component renderer
class ComponentRenderer {
render(component: DynamicComponent): void {
console.log(`\n๐จ Rendering ${component.type} component:`);
if (isButton(component)) {
this.renderButton(component);
} else if (isInput(component)) {
this.renderInput(component);
} else if (isSelect(component)) {
this.renderSelect(component);
} else {
// Exhaustive check
const _never: never = component;
throw new Error(`Unknown component type: ${_never}`);
}
}
private renderButton(button: ButtonComponent): void {
const variant = button.variant || "primary";
const emoji = {
primary: "๐ต",
secondary: "โช",
danger: "๐ด"
}[variant];
console.log(`${emoji} [${button.text}] (${variant} button)`);
console.log(` Click handler attached ๐`);
}
private renderInput(input: InputComponent): void {
const type = input.inputType || "text";
const emoji = {
text: "๐",
password: "๐",
email: "๐ง"
}[type];
console.log(`${emoji} Input (${type})`);
console.log(` Placeholder: "${input.placeholder}"`);
console.log(` Value: "${input.value}"`);
}
private renderSelect(select: SelectComponent): void {
console.log(`๐ Select dropdown`);
console.log(` Current: "${select.value}"`);
console.log(` Options:`);
select.options.forEach(opt => {
const check = opt.value === select.value ? "โ
" : "โฌ";
console.log(` ${check} ${opt.label} (${opt.value})`);
});
}
}
// ๐ฎ Test the renderer
const renderer = new ComponentRenderer();
const components: DynamicComponent[] = [
{
type: "button",
text: "Submit Form",
onClick: () => console.log("Form submitted!"),
variant: "primary"
},
{
type: "input",
placeholder: "Enter your email",
value: "[email protected]",
onChange: (v) => console.log(`Email changed: ${v}`),
inputType: "email"
},
{
type: "select",
options: [
{ value: "ts", label: "TypeScript" },
{ value: "js", label: "JavaScript" },
{ value: "py", label: "Python" }
],
value: "ts",
onChange: (v) => console.log(`Selected: ${v}`)
}
];
components.forEach(component => renderer.render(component));
๐ Advanced Concepts
๐งโโ๏ธ Advanced Type Guard Patterns
Level up with sophisticated type guards:
// ๐ฏ Generic type guard factory
function createTypeGuard<T>(
validator: (value: unknown) => boolean
): (value: unknown) => value is T {
return (value: unknown): value is T => validator(value);
}
// ๐จ Schema-based validation
interface Schema {
[key: string]: "string" | "number" | "boolean" | Schema;
}
function validateSchema<T>(data: unknown, schema: Schema): data is T {
if (typeof data !== "object" || data === null) return false;
for (const [key, type] of Object.entries(schema)) {
const value = (data as any)[key];
if (typeof type === "object") {
if (!validateSchema(value, type)) return false;
} else if (typeof value !== type) {
return false;
}
}
return true;
}
// Usage
interface User {
name: string;
age: number;
preferences: {
theme: string;
notifications: boolean;
};
}
const userSchema: Schema = {
name: "string",
age: "number",
preferences: {
theme: "string",
notifications: "boolean"
}
};
const data: unknown = {
name: "Alice",
age: 30,
preferences: {
theme: "dark",
notifications: true
}
};
if (validateSchema<User>(data, userSchema)) {
console.log(`๐ค Valid user: ${data.name}`);
}
// ๐ Branded types for extra safety
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<number, "UserId">;
type ProductId = Brand<string, "ProductId">;
function isUserId(value: unknown): value is UserId {
return typeof value === "number" && value > 0;
}
function isProductId(value: unknown): value is ProductId {
return typeof value === "string" && value.startsWith("PROD-");
}
// Type-safe ID usage
function getUser(id: UserId): void {
console.log(`Fetching user ${id}`);
}
const maybeUserId: unknown = 123;
if (isUserId(maybeUserId)) {
getUser(maybeUserId); // Type safe!
}
๐๏ธ Assertion Functions
TypeScript 3.7+ assertion functions:
// ๐ก๏ธ Assertion functions narrow types
function assertString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error(`Expected string, got ${typeof value}`);
}
}
function assertNonNull<T>(value: T | null | undefined): asserts value is T {
if (value == null) {
throw new Error("Value cannot be null or undefined");
}
}
function assertUserRole(
user: { role: string },
allowedRoles: readonly string[]
): asserts user is { role: typeof allowedRoles[number] } {
if (!allowedRoles.includes(user.role)) {
throw new Error(`Invalid role: ${user.role}`);
}
}
// ๐ฎ Usage - type narrowing after assertion
function processData(data: unknown) {
assertString(data);
// TypeScript knows data is string now!
console.log(data.toUpperCase());
}
function processUser(user: { role: string } | null) {
assertNonNull(user);
assertUserRole(user, ["admin", "moderator"] as const);
// TypeScript knows user.role is "admin" | "moderator"
if (user.role === "admin") {
console.log("๐ Admin privileges granted");
}
}
// ๐จ Custom assertion with detailed errors
class ValidationError extends Error {
constructor(
public field: string,
public expected: string,
public actual: unknown
) {
super(`${field}: expected ${expected}, got ${typeof actual}`);
}
}
function assertValidEmail(value: unknown): asserts value is string {
assertString(value);
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
throw new ValidationError("email", "valid email format", value);
}
}
๐จ Template Literal Type Guards
// ๐งฌ Advanced string pattern matching
type HexColor = `#${string}`;
type RGBColor = `rgb(${number}, ${number}, ${number})`;
type Color = HexColor | RGBColor | "transparent";
function isHexColor(value: string): value is HexColor {
return /^#[0-9A-Fa-f]{6}$/.test(value);
}
function isRGBColor(value: string): value is RGBColor {
return /^rgb\(\d{1,3}, \d{1,3}, \d{1,3}\)$/.test(value);
}
function parseColor(color: string): Color | null {
if (isHexColor(color)) {
console.log(`๐จ Hex color: ${color}`);
return color;
} else if (isRGBColor(color)) {
console.log(`๐ RGB color: ${color}`);
return color;
} else if (color === "transparent") {
console.log(`๐ป Transparent`);
return color;
}
console.error(`โ Invalid color: ${color}`);
return null;
}
// ๐ฏ URL pattern matching
type HTTPUrl = `http://${string}`;
type HTTPSUrl = `https://${string}`;
type URL = HTTPUrl | HTTPSUrl;
function isSecureUrl(url: string): url is HTTPSUrl {
return url.startsWith("https://");
}
function fetchSecure(url: URL): void {
if (isSecureUrl(url)) {
console.log(`๐ Secure fetch: ${url}`);
} else {
console.warn(`โ ๏ธ Insecure URL: ${url}`);
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Unsafe Type Assertions
// โ Wrong - lying to TypeScript!
const data: unknown = { name: "Alice" };
const user = data as { id: number; email: string };
console.log(user.id); // undefined at runtime! ๐ฅ
// โ
Correct - validate before asserting
if (isValidUser(data)) {
console.log(user.id); // Safe!
}
// ๐ฏ Better - use type guards
function getUser(data: unknown): User | null {
if (isUser(data)) {
return data;
}
return null;
}
๐คฏ Pitfall 2: Double Assertions
// โ Very dangerous - bypassing all safety!
const value = "hello" as unknown as number;
// TypeScript thinks it's a number, but it's a string!
// โ
If you must assert, validate first
function toNumber(value: unknown): number {
const num = Number(value);
if (isNaN(num)) {
throw new Error(`Cannot convert ${value} to number`);
}
return num;
}
๐ต Pitfall 3: Incomplete Type Guards
// โ Incomplete guard - missing properties
function badIsUser(value: unknown): value is User {
return typeof value === "object" && value !== null;
// Forgot to check for required properties!
}
// โ
Complete guard - check all properties
function goodIsUser(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 === "number" &&
typeof (value as User).name === "string" &&
typeof (value as User).email === "string"
);
}
๐ค Pitfall 4: Type Guard Logic Errors
// โ Wrong logic in type guard
function isStringOrNumber(value: unknown): value is string | number {
// This is AND, not OR!
return typeof value === "string" && typeof value === "number";
// Always returns false!
}
// โ
Correct logic
function isStringOrNumber(value: unknown): value is string | number {
return typeof value === "string" || typeof value === "number";
}
// ๐ฏ Even better - separate guards
function processValue(value: unknown) {
if (typeof value === "string") {
console.log("String:", value.toUpperCase());
} else if (typeof value === "number") {
console.log("Number:", value.toFixed(2));
}
}
๐ฌ Pitfall 5: Forgetting About Arrays
// โ Forgetting arrays are objects too
function isObject(value: unknown): value is object {
return typeof value === "object" && value !== null;
// Arrays will pass this check!
}
const arr = [1, 2, 3];
if (isObject(arr)) {
// arr is treated as object, not array!
}
// โ
Distinguish between arrays and objects
function isPlainObject(value: unknown): value is Record<string, unknown> {
return (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
!(value instanceof Date) &&
!(value instanceof RegExp)
);
}
// ๐ฏ Specific checks
if (Array.isArray(value)) {
console.log("๐ It's an array");
} else if (isPlainObject(value)) {
console.log("๐ฆ It's a plain object");
}
๐ ๏ธ Best Practices
1. ๐ฏ Prefer Type Guards Over Assertions
// โ
Good - runtime validation
function processData(data: unknown) {
if (isUser(data)) {
console.log(data.name); // Safe!
}
}
// โ Avoid - no runtime check
function processDataBad(data: unknown) {
const user = data as User; // Dangerous!
console.log(user.name);
}
2. ๐ Create Reusable Type Guards
// โ
Good - reusable validation
const typeGuards = {
isString: (value: unknown): value is string =>
typeof value === "string",
isNumber: (value: unknown): value is number =>
typeof value === "number" && !isNaN(value),
isNonEmptyString: (value: unknown): value is string =>
typeof value === "string" && value.length > 0,
isPositiveNumber: (value: unknown): value is number =>
typeof value === "number" && value > 0,
isEmail: (value: unknown): value is string =>
typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
};
3. ๐ก๏ธ Use Assertion Functions for Critical Paths
// โ
Good - fail fast for critical errors
function assertDefined<T>(
value: T | null | undefined,
message?: string
): asserts value is T {
if (value == null) {
throw new Error(message || "Value must be defined");
}
}
// Usage
const config = getConfig();
assertDefined(config, "Configuration is required");
// TypeScript knows config is defined here
4. ๐จ Combine Guards for Complex Types
// โ
Good - compose simple guards
function isUserWithPremium(value: unknown): value is User & { premium: true } {
return isUser(value) &&
"premium" in value &&
value.premium === true;
}
function hasRequiredFields<T extends object>(
obj: unknown,
fields: (keyof T)[]
): obj is T {
if (typeof obj !== "object" || obj === null) return false;
return fields.every(field => field in obj);
}
5. โจ Document Your Assertions
// โ
Good - clear documentation
/**
* Asserts that a value is a valid User object.
* @throws {ValidationError} If validation fails
* @example
* assertUser(data); // Throws if invalid
* console.log(data.name); // Safe to use
*/
function assertUser(value: unknown): asserts value is User {
if (!isUser(value)) {
throw new ValidationError("User", value);
}
}
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Type-Safe JSON Parser
Create a JSON parser with full type safety and validation:
๐ Requirements:
- โ Parse JSON strings with type validation
- ๐ท๏ธ Support nested objects and arrays
- ๐ค Custom type guards for data shapes
- ๐ Handle dates and special types
- ๐จ Detailed error reporting
๐ Bonus Points:
- Add schema validation
- Implement transformation pipelines
- Create type inference from schemas
๐ก Solution
๐ Click to see solution
// ๐ฏ Type-safe JSON parser with validation
// Schema definition types
type SchemaType =
| "string"
| "number"
| "boolean"
| "date"
| "array"
| "object"
| "null";
interface SchemaNode {
type: SchemaType;
optional?: boolean;
items?: SchemaNode; // For arrays
properties?: Record<string, SchemaNode>; // For objects
validate?: (value: any) => boolean;
transform?: (value: any) => any;
}
// Type guards for primitives
const primitiveGuards = {
string: (v: unknown): v is string => typeof v === "string",
number: (v: unknown): v is number => typeof v === "number" && !isNaN(v),
boolean: (v: unknown): v is boolean => typeof v === "boolean",
null: (v: unknown): v is null => v === null,
date: (v: unknown): v is Date => {
if (v instanceof Date) return !isNaN(v.getTime());
if (typeof v === "string") {
const date = new Date(v);
return !isNaN(date.getTime());
}
return false;
}
};
// ๐ก๏ธ JSON Parser class
class TypeSafeJsonParser {
private errors: string[] = [];
parse<T>(json: string, schema: SchemaNode): T | null {
this.errors = [];
try {
const data = JSON.parse(json);
const validated = this.validate(data, schema, "root");
if (this.errors.length > 0) {
console.error("โ Validation errors:");
this.errors.forEach(err => console.error(` - ${err}`));
return null;
}
return validated as T;
} catch (error) {
console.error("โ JSON parse error:", error);
return null;
}
}
private validate(value: unknown, schema: SchemaNode, path: string): unknown {
// Handle optional values
if (value === undefined || value === null) {
if (schema.optional) return null;
if (value === null && schema.type === "null") return null;
this.errors.push(`${path}: required value is missing`);
return null;
}
// Validate based on type
switch (schema.type) {
case "string":
case "number":
case "boolean":
case "null":
if (!primitiveGuards[schema.type](value)) {
this.errors.push(`${path}: expected ${schema.type}, got ${typeof value}`);
return null;
}
break;
case "date":
if (!primitiveGuards.date(value)) {
this.errors.push(`${path}: invalid date value`);
return null;
}
// Transform string to Date
value = new Date(value as string);
break;
case "array":
if (!Array.isArray(value)) {
this.errors.push(`${path}: expected array, got ${typeof value}`);
return null;
}
if (schema.items) {
value = value.map((item, index) =>
this.validate(item, schema.items!, `${path}[${index}]`)
);
}
break;
case "object":
if (typeof value !== "object" || value === null || Array.isArray(value)) {
this.errors.push(`${path}: expected object, got ${typeof value}`);
return null;
}
if (schema.properties) {
const result: Record<string, unknown> = {};
// Validate defined properties
for (const [key, propSchema] of Object.entries(schema.properties)) {
result[key] = this.validate(
(value as any)[key],
propSchema,
`${path}.${key}`
);
}
// Check for extra properties
for (const key of Object.keys(value as object)) {
if (!(key in schema.properties)) {
this.errors.push(`${path}: unexpected property "${key}"`);
}
}
value = result;
}
break;
}
// Custom validation
if (schema.validate && !schema.validate(value)) {
this.errors.push(`${path}: custom validation failed`);
return null;
}
// Custom transformation
if (schema.transform) {
value = schema.transform(value);
}
return value;
}
// ๐ฏ Schema builder helpers
static schema = {
string: (optional = false): SchemaNode => ({ type: "string", optional }),
number: (optional = false): SchemaNode => ({ type: "number", optional }),
boolean: (optional = false): SchemaNode => ({ type: "boolean", optional }),
date: (optional = false): SchemaNode => ({ type: "date", optional }),
null: (): SchemaNode => ({ type: "null" }),
array: <T>(items: SchemaNode, optional = false): SchemaNode => ({
type: "array",
items,
optional
}),
object: (properties: Record<string, SchemaNode>, optional = false): SchemaNode => ({
type: "object",
properties,
optional
}),
// Advanced helpers
email: (optional = false): SchemaNode => ({
type: "string",
optional,
validate: (v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)
}),
enum: <T extends string>(values: readonly T[], optional = false): SchemaNode => ({
type: "string",
optional,
validate: (v: string) => values.includes(v as T)
}),
range: (min: number, max: number, optional = false): SchemaNode => ({
type: "number",
optional,
validate: (v: number) => v >= min && v <= max
})
};
}
// ๐ฎ Test the parser
const parser = new TypeSafeJsonParser();
const { schema } = TypeSafeJsonParser;
// Define a user schema
interface User {
id: number;
name: string;
email: string;
age: number;
role: "admin" | "user" | "guest";
preferences: {
theme: "light" | "dark";
notifications: boolean;
};
tags: string[];
createdAt: Date;
deletedAt: Date | null;
}
const userSchema: SchemaNode = schema.object({
id: schema.number(),
name: schema.string(),
email: schema.email(),
age: schema.range(0, 150),
role: schema.enum(["admin", "user", "guest"] as const),
preferences: schema.object({
theme: schema.enum(["light", "dark"] as const),
notifications: schema.boolean()
}),
tags: schema.array(schema.string()),
createdAt: schema.date(),
deletedAt: schema.date(true) // Optional
});
// Test valid JSON
const validJson = `{
"id": 123,
"name": "Alice Smith",
"email": "[email protected]",
"age": 28,
"role": "admin",
"preferences": {
"theme": "dark",
"notifications": true
},
"tags": ["typescript", "developer"],
"createdAt": "2024-01-15T10:30:00Z",
"deletedAt": null
}`;
const user = parser.parse<User>(validJson, userSchema);
if (user) {
console.log("โ
Parsed user:", user);
console.log(`๐ค ${user.name} (${user.role})`);
console.log(`๐
Created: ${user.createdAt.toLocaleDateString()}`);
}
// Test invalid JSON
const invalidJson = `{
"id": "not-a-number",
"name": "Bob",
"email": "invalid-email",
"age": 200,
"role": "superadmin",
"preferences": {
"theme": "blue"
},
"tags": ["developer", 123],
"createdAt": "invalid-date"
}`;
const invalidUser = parser.parse<User>(invalidJson, userSchema);
// Will log validation errors
// ๐ฏ Create a config schema
const configSchema = schema.object({
api: schema.object({
endpoint: schema.string(),
timeout: schema.range(1000, 60000),
retries: schema.range(0, 5, true)
}),
features: schema.array(schema.enum(["auth", "analytics", "notifications"] as const)),
debug: schema.boolean(true)
});
const configJson = `{
"api": {
"endpoint": "https://api.example.com",
"timeout": 5000
},
"features": ["auth", "analytics"],
"debug": false
}`;
const config = parser.parse(configJson, configSchema);
if (config) {
console.log("\n๐ง Parsed config:", config);
}
๐ Key Takeaways
Youโve mastered type assertions and type guards! Hereโs what you can now do:
- โ Use type assertions responsibly when you know better ๐ช
- โ Create custom type guards for runtime validation ๐ก๏ธ
- โ Narrow union types with built-in guards ๐ฏ
- โ Handle unknown data safely and confidently ๐
- โ Build bulletproof APIs with proper validation! ๐
Remember: Type assertions say โtrust me,โ type guards say โlet me check.โ Always prefer checking! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered type assertions and type guards!
Hereโs what to do next:
- ๐ป Complete the JSON parser exercise
- ๐๏ธ Add type guards to your existing projects
- ๐ Move on to our next tutorial: The any, unknown, never, and void Types
- ๐ Create a validation library with your new skills!
Remember: Safe code is happy code. Type guards are your friends - use them liberally! ๐
Happy coding! ๐๐โจ