Steve Kinney

Type Guards vs Schema Validation

Type guards are way to use runtime logic to help put TypeScript at ease that a given object is actually the type you think it ought to be. Here is a quick example.

interface Circle {
  kind: 'circle';
  radius: number;
}

interface Square {
  kind: 'square';
  side: number;
}

type Shape = Circle | Square;

function isCircle(shape: Shape): shape is Circle {
  return shape.kind === 'circle';
}

function calculateArea(shape: Shape) {
  if (isCircle(shape)) {
    return Math.PI * shape.radius ** 2;
  }
  return shape.side ** 2;
}

A previous iteration of me might have thought he was was really smart when he made a type guard like this.

// Define a type guard for User objects
function isUser(obj: any): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    typeof obj.id === 'string' &&
    typeof obj.username === 'string' &&
    typeof obj.email === 'string'
  );
}

Alternatively, you could do something like this.

// Runtime type checking can impact performance
function validateUser(data: unknown): User {
  if (
    typeof data !== 'object' ||
    data === null ||
    !('username' in data) ||
    !('email' in data) ||
    typeof data.username !== 'string' ||
    typeof data.email !== 'string'
  ) {
    throw new ValidationError('Invalid user data');
  }

  return data as User;
}

Schema validation libraries are optimized for performance, making them better than hand-rolled validation. Let’s look at the same idea, but using Zod.

// More efficient with schema validation libraries
const userSchema = z.object({
  username: z.string(),
  email: z.string().email(),
});

function validateUser(data: unknown): User {
  return userSchema.parse(data);
}

Last modified on .