Tipsy: Format Validation

By Josh Britz | 6 June 2025

Have you ever wanted to add a type in TypeScript to ensure that a property is not only of the correct type, but also of the correct format? Fortunately string literals come to the rescue here. Let's look at this example:

interface User {
  name: string;
  phone: string;
  email: string;
}

const user: User = {
  name: 'Josh',
  phone: '000 000 0000',
  email: 'josh@email.com',
}; // ✅ Passes type checking

const user2: User = {
  name: 'James',
  phone: 'hello',
  email: 'world',
}; // ✅ Also passes type checking

Now this can be a problem because unallowable values are not invalidated at build time. But here is where string literals come to the rescue. We can define specific structure generically in TypeScript like this.

interface User {
  name: string;
  phone: `${number} ${number} ${number}`;
  email: `${string}@${string}.${string}`;
}

const user: User = {
  name: 'Josh',
  phone: '000 000 0000',
  email: 'josh@email.com',
}; // ✅ Passes type checking

const user2: User = {
  name: 'James',
  phone: 'hello',
  email: 'world',
}; // ❌ Fails type checking

As a bonus, here are some useful common types:

type PhoneCode = `+(${number})`;
type UUID = `${string}-${string}-${string}-${string}-${string}`;

// CSS Values
type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
type CSSValue<T extends number> = `${T}${CSSUnit}`;

// Smart log formatting
type ComponentName = string;
type LogMessage<Component extends ComponentName> = `[${Component}] ${string}`;

function log<T extends ComponentName>(
  component: T,
  message: string,
): LogMessage<T> {
  return `[${component}] ${message}`;
}