Posted in

Why Custom Types? Your Data’s Best Friend

TypeScript Custom Types: Your Data's Best Friend
What is Custom TypeScript? Your Data's Best Friend

When you are building applications with TypeScript, you often work with data that has a specific structure. While TypeScript’s built-in types (like string, number, boolean) are great for simple values, what happens when you have more complex data, like an object representing a user with a name, email, and a list of orders? This is where custom types come in. They allow you to define your own blueprints for data, making your code safer, more readable, and easier to maintain.

You know that feeling when you are cooking without a recipe? You think you know what ingredients you need, but halfway through you realize you are missing something crucial, or worse—you have added salt instead of sugar. That’s exactly what coding without TypeScript custom types feels like.

The “Oh Crap” Moment We’ve All Had

Let me tell you about the time I spent three hours debugging why my user dashboard was showing “undefined” everywhere. Turns out, the API was returning firstName but my code was expecting name. Three. Whole. Hours. For a typo!

// This was my nightmare - no type safety
const userData: any = {
  firstName: "Sarah",
  age: 28,
  emailAddress: "sarah@example.com"
};

// I kept doing this and wondering why nothing worked
console.log(userData.name); // undefined... obviously!
console.log(userData.email); // undefined again... *facepalm*

Sound familiar? We have all been there. The worst part? The code runs fine until it doesn’t, and then you are left wondering where everything went wrong, frantically searching for that needle in a haystack.

Enter Custom Types: Your Code’s Guardian Angel

Think of custom types as having a really smart friend who always remembers things you forget. They are like that friend who says, “Hey, didn’t you mean to use firstName instead of name?” before you embarrass yourself.

// This is your smart friend in code form
type User = {
  firstName: string;
  age: number;
  emailAddress: string;
};

const userData: User = {
  firstName: "Sarah",
  age: 28,
  emailAddress: "sarah@example.com"
};

// Now if you try to be clever and use the wrong property name...
// console.log(userData.name); // TypeScript: "Nope, not happening!"

The moment you try to access userData.name, TypeScript throws up a big red flag. It’s like having a spell-checker for your data structures, telling you exactly what’s off before you even run your code. This isn’t just about catching typos; it’s about building confidence in your codebase.

Building Your First Custom Type (It’s Easier Than You Think)

Creating custom types is like writing a shopping list for your data. You are just telling TypeScript, “Here’s what I expect this thing to look like.”

// It's literally this simple
type Recipe = {
  name: string;
  ingredients: string[]; // An array of strings, like ["flour", "milk"]
  cookingTime: number; // In minutes, maybe?
  difficulty: "easy" | "medium" | "hard"; // Specific allowed values!
};

// Now you can use it
const pancakes: Recipe = {
  name: "Fluffy Pancakes",
  ingredients: ["flour", "milk", "eggs", "sugar"],
  cookingTime: 15,
  difficulty: "easy"
};

// TypeScript has your back if you mess up
/*
const messedUpRecipe: Recipe = {
  title: "Oops", // Error! It should be 'name'
  stuff: [], // Error! It should be 'ingredients'
  time: 30, // Error! It should be 'cookingTime'
  level: "hard" // Error! It should be 'difficulty', and spelling counts!
};
*/

See? TypeScript is like that friend who proofreads your text messages before you send them, saving you from awkward typos. It ensures that every Recipe object you create adheres to this exact structure, no surprises.

Real-World Examples (Because Theory is Boring)

The E-commerce Store That Actually Works

Let’s say you are building an online store. Without custom types, you had be flying blind, just like I was with my userData fiasco:

// The old way - hope and pray everything matches
const product1 = {
  id: "shirt-001",
  name: "Cool T-Shirt",
  price: 25.99,
  inStock: true
};

const product2 = {
  productId: "pants-002", // Wait, is it 'id' or 'productId'?
  title: "Jeans", // And is it 'name' or 'title'?
  cost: 49.99, // Or 'price' vs 'cost'?
  available: false // 'inStock' vs 'available'... help!
};

This is a recipe for disaster, especially when different developers are working on different parts of the system. Now, with custom types, the entire team know exactly what’s expected:

type Product = {
  id: string;
  name: string;
  price: number;
  inStock: boolean;
  category: string;
  description: string;
};

// Now everyone on your team knows exactly what a Product looks like
const coolShirt: Product = {
  id: "shirt-001",
  name: "Cool T-Shirt",
  price: 25.99,
  inStock: true,
  category: "clothing",
  description: "The coolest shirt you'll ever wear"
};

// Try to mess it up and TypeScript will stop you
/*
const confusedProduct: Product = {
  productId: "broken", // Error! Use 'id' dummy
  // Missing required fields? TypeScript will tell you exactly what's missing
};
*/

This clarity isn’t just for avoiding errors; it boosts team productivity. No more wasted time debating property names or digging through documentation. The type itself is the documentation.

The User Profile That Makes Sense

Let’s look at a user profile. It often contains a mix of required and optional data, and even nested information.

type UserProfile = {
  id: string;
  username: string;
  email: string;
  joinDate: Date;
  preferences: { // This is a nested object!
    theme: "light" | "dark"; // Union type for specific values
    notifications: boolean;
    language: string;
  };
};

// Crystal clear what this object should contain
const newUser: UserProfile = {
  id: "user-12345",
  username: "javascriptjunkie",
  email: "js@example.com",
  joinDate: new Date(),
  preferences: {
    theme: "dark", // Only 'light' or 'dark' allowed
    notifications: true,
    language: "en"
  }
};

This structure immediately tells you that newUser has an id, username, email, a joinDate that must be a Date object, and a preferences object that itself has specific properties. It’s like a perfectly organized filing cabinet for your data.

Going Deeper with Custom Types

Custom types are incredibly flexible. Let’s explore some more common scenarios.

Optional Properties: For When Life Gets Complicated

Sometimes you don’t have all the information upfront. Maybe a user hasn’t uploaded a profile picture yet, or a product doesn’t have reviews. That’s where optional properties come in—they are like saying “this might be here, or it might not, and that’s okay.” You add a ? after the property name.

type BlogPost = {
  id: string;
  title: string;
  content: string;
  publishDate: Date;
  author: string;
  tags: string[];
  featuredImage?: string; // Maybe there's an image URL, maybe not
  comments?: Comment[]; // Maybe there are comments (an array of Comment objects), maybe not
  likes?: number; // Maybe someone liked it, maybe not
};

type Comment = { // A simple nested type for comments
    id: string;
    text: string;
    author: string;
    timestamp: Date;
}

// This works fine without the optional stuff
const quickPost: BlogPost = {
  id: "post-001",
  title: "Why I Love TypeScript",
  content: "Because it saves me from myself!",
  publishDate: new Date(),
  author: "TypeScript Fan",
  tags: ["typescript", "javascript", "programming"]
  // No featuredImage, comments, or likes - and that's perfectly fine!
};

// This also works with optional properties included
const fullPost: BlogPost = {
  id: "post-002",
  title: "Complete Guide to Custom Types",
  content: "Everything you need to know...",
  publishDate: new Date(),
  author: "Ayush Kumar Tiwari",
  tags: ["typescript", "tutorial"],
  featuredImage: "https://example.com/image.jpg",
  comments: [
    { id: "c1", text: "Great post!", author: "Reader1", timestamp: new Date() },
    { id: "c2", text: "Very helpful!", author: "Reader2", timestamp: new Date() }
  ],
  likes: 42
};

This is a lifesaver when dealing with evolving data or data that comes from various sources where some fields might legitimately be absent.

Nesting Types: Building Complex Structures

Real applications are not simple. You have got users who have addresses, orders that have items, posts that have authors. Custom types let you build these complex relationships in a way that actually makes sense, by nesting one type inside another.

// Start with the building blocks
type Address = {
  street: string;
  city: string;
  state: string;
  zipCode: string;
  country: string;
};

type PaymentMethod = {
  type: "credit" | "debit" | "paypal"; // Specific payment types
  last4?: string; // Optional for privacy/security
  expiryDate?: string;
};

// Now build something more complex: a Customer
type Customer = {
  id: string;
  name: string;
  email: string;
  phone?: string;
  addresses: Address[]; // A customer can have multiple addresses (an array of Address types!)
  paymentMethods: PaymentMethod[]; // Multiple ways to pay
  orderHistory: string[]; // List of order IDs
  createdAt: Date;
  isActive: boolean;
};

// Look how clean and understandable this is
const loyalCustomer: Customer = {
  id: "cust-001",
  name: "Jane Smith",
  email: "jane@example.com",
  phone: "555-0123",
  addresses: [
    {
      street: "123 Main St",
      city: "Anytown",
      state: "CA",
      zipCode: "12345",
      country: "USA"
    },
    {
      street: "456 Work Ave",
      city: "Business City",
      state: "NY",
      zipCode: "67890",
      country: "USA"
    }
  ],
  paymentMethods: [
    {
      type: "credit",
      last4: "1234",
      expiryDate: "12/25"
    }
  ],
  orderHistory: ["order-001", "order-002", "order-003"],
  createdAt: new Date("2023-01-15T10:00:00Z"), // ISO format for dates is good practice
  isActive: true
};

This hierarchical structure mirrors real-world relationships, making your code not just functional but also incredibly intuitive to read and reason about.

Union Types: When You Need Options

Sometimes a property can be one of several different things. Like a status that can be “pending”, “approved”, or “rejected”. Union types let you be specific about what’s allowed, using the | symbol.

type OrderStatus = "pending" | "confirmed" | "shipped" | "delivered" | "cancelled";

type Order = {
  id: string;
  customerId: string;
  items: string[];
  total: number;
  status: OrderStatus; // Only these specific values are allowed
  shippingAddress: Address; // Reusing our Address type!
  orderDate: Date;
  estimatedDelivery?: Date;
};

const newOrder: Order = {
  id: "order-001",
  customerId: "cust-001",
  items: ["item-1", "item-2"],
  total: 99.99,
  status: "pending", // TypeScript ensures this is one of the allowed values
  shippingAddress: {
    street: "123 Main St",
    city: "Anytown",
    state: "CA",
    zipCode: "12345",
    country: "USA"
  },
  orderDate: new Date()
};

// Try to set an invalid status and TypeScript will stop you
// newOrder.status = "maybe"; // Error! This isn't one of the allowed values in OrderStatus

This is fantastic for enumerations or states where you want to restrict the possible values, preventing typos and unexpected data.

Type Assertions: When You Know Better Than TypeScript

Sometimes you are getting data from an API or another source, and you know more about what it contains than TypeScript does. That’s when you use type assertions—but use them carefully! You use the as keyword.

// Let's say you're getting data from an API, and TypeScript sees it as 'unknown' or 'any'
const apiResponse: unknown = { // 'unknown' is safer than 'any' for incoming data
  id: "user-123",
  name: "Ayu",
  email: "ayu@example.com",
  age: 24
};

// You know this is actually a User, so you can tell TypeScript
type User = {
  id: string;
  name: string;
  email: string;
  age: number;
};

const user = apiResponse as User; // We are asserting 'apiResponse' has the shape of 'User'
console.log(user.name); // Now TypeScript knows this is safe, and provides autocomplete!

// But be careful! If you're wrong, you'll get runtime errors
const wrongAssertion = "just a string" as User;
// console.log(wrongAssertion.name); // This will blow up at runtime! There's no 'name' property on a string!

Pro tip: Only use type assertions when you are absolutely certain about the data structure. It’s like telling TypeScript “trust me, I know what I’m doing”—which is famous last words in programming! A better approach, if possible, is to validate incoming data at runtime and then assert, but for quick and dirty cases, this works.

Arrays and Custom Types: Handling Lists of Things

Most applications deal with lists of things—users, products, orders, whatever. Custom types make working with arrays much cleaner and more explicit.

type Task = {
  id: string;
  title: string;
  description: string;
  completed: boolean;
  priority: "low" | "medium" | "high";
  dueDate?: Date;
  assignedTo?: string;
};

type Project = {
  id: string;
  name: string;
  description: string;
  tasks: Task[]; // Array of Task objects – perfectly clear!
  teamMembers: string[]; // Array of team member names (strings)
  startDate: Date;
  endDate?: Date;
  status: "planning" | "active" | "completed" | "cancelled";
};

const myProject: Project = {
  id: "proj-001",
  name: "Build Awesome App",
  description: "Creating the next big thing",
  tasks: [
    {
      id: "task-001",
      title: "Design user interface",
      description: "Create wireframes and mockups",
      completed: false,
      priority: "high",
      dueDate: new Date("2024-07-15"),
      assignedTo: "designer@company.com"
    },
    {
      id: "task-002",
      title: "Set up database",
      description: "Configure PostgreSQL database",
      completed: true,
      priority: "medium"
    }
  ],
  teamMembers: ["dev1@company.com", "dev2@company.com", "designer@company.com"],
  startDate: new Date("2024-07-01"),
  endDate: new Date("2024-09-30"),
  status: "active"
};

This ensures that every element in the tasks array adheres to the Task blueprint, preventing malformed task data from creeping into your project.

Function Types: Making Sure Functions Play Nice

You can also create custom types for functions, which is incredibly useful for callbacks, event handlers, and any function that gets passed around. This guarantees consistency in how your functions are defined and used.

// Define what a validation function should look like: takes a string, returns a boolean
type ValidationFunction = (value: string) => boolean;

// Define what a click event handler should look like: takes a MouseEvent, returns nothing (void)
type ClickHandler = (event: MouseEvent) => void;

// Define what an API call function should look like: takes an endpoint string, returns a Promise of type T
type ApiCall<T> = (endpoint: string) => Promise<T>;

// Now you can use these types
const validateEmail: ValidationFunction = (email) => {
  return email.includes('@') && email.includes('.');
};

const handleButtonClick: ClickHandler = (event) => {
  console.log('Button clicked!', event.target);
};

// We'll use our `User` type from before as the generic `T`
const fetchUser: ApiCall<User> = async (endpoint) => {
  const response = await fetch(endpoint);
  if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
  }
  return response.json();
};

// Usage:
// const myUser = await fetchUser('/api/user/123');
// console.log(myUser.name);

This enforces the signature of your functions, making them more predictable and easier to integrate into different parts of your application.

Generic Types: One Size Fits Many

Sometimes you want to create a type that works with different kinds of data without having to rewrite the whole type definition. Generics let you do this by using a placeholder type variable (often T), making your types incredibly reusable.

// A generic response type that works with *any* data type
type ApiResponse<T> = { // 'T' is our placeholder for whatever data type we pass in
  data: T; // The 'data' property will be of type T
  success: boolean;
  message: string;
  timestamp: Date;
};

// Now you can use it with any specific type of data
const userResponse: ApiResponse<User> = { // T becomes 'User' here
  data: { id: "1", firstName: "Iti", age: 23, emailAddress: "john@example.com" },
  success: true,
  message: "User retrieved successfully",
  timestamp: new Date()
};

const productResponse: ApiResponse<Product[]> = { // T becomes 'Product[]' here (an array of Products)
  data: [
    { id: "1", name: "Laptop", price: 999, inStock: true, category: "Electronics", description: "Great laptop" },
    { id: "2", name: "Mouse", price: 29, inStock: true, category: "Electronics", description: "Wireless mouse" }
  ],
  success: true,
  message: "Products retrieved successfully",
  timestamp: new Date()
};

Generics are a superpower for building flexible and scalable APIs and data structures. They allow you to define a pattern once and apply it to many different data scenarios.

Common Mistakes (And How to Avoid Them)

Even with the best intentions, it’s easy to fall into a few traps when using custom types. Here’s what I have learned from experience:

1. Making Everything Required

It’s tempting to make every single property mandatory. But what if some data simply isn’t available sometimes?

// Don't do this - too rigid
/*
type User = {
  id: string;
  name: string;
  email: string;
  phone: string; // What if they don't have a phone?
  address: string; // What if they haven't provided an address?
  profilePicture: string; // What if they haven't uploaded one?
};
*/

// Do this instead - make optional what should be optional
type UserContactInfo = {
  id: string;
  name: string;
  email: string;
  phone?: string; // Add '?' for optional
  address?: string;
  profilePicture?: string;
};

Making properties optional when they truly are helps you avoid annoying errors and makes your types reflect the real world.

2. Using any As a Crutch

We talked about any before – it’s like telling TypeScript, “just leave me alone!” While it has its place for very specific scenarios (like integrating with legacy JavaScript), relying on any defeats the whole purpose of TypeScript.

// Don't do this - defeats the purpose
/*
type User = {
  id: string;
  name: string;
  metadata: any; // This is basically giving up on type safety for 'metadata'
};
*/

// Do this instead - be specific! Even if it's nested
type UserWithMetadata = {
  id: string;
  name: string;
  metadata: { // Define the structure of metadata
    lastLogin?: Date;
    preferences: {
      theme: "light" | "dark";
      notifications: boolean;
    };
    tags: string[];
  };
};

The more specific you are, the more TypeScript can help you. Don’t be lazy; define those types!

3. Over-Complicated Nesting

While nesting is powerful, don’t overdo it. Sometimes, breaking down a super-nested type into smaller, more manageable custom types makes the code much clearer.

// Don't do this - too complex to read at a glance
/*
type OverComplicated = {
  data: {
    user: {
      profile: {
        personal: {
          name: {
            first: string;
            last: string;
          };
        };
      };
    };
  };
};
*/

// Do this instead - break it down into smaller, readable types
type FirstNameAndLastName = {
  first: string;
  last: string;
};

type PersonalInfo = {
  name: FirstNameAndLastName;
};

type UserProfileDetails = { // Renamed for clarity
  personal: PersonalInfo;
};

type UserDataContainer = { // A container for user data
  user: {
    profile: UserProfileDetails;
  };
};

// Example usage:
const myComplexData: UserDataContainer = {
    data: {
        user: {
            profile: {
                personal: {
                    name: {
                        first: "Ayu",
                        last: "Developer"
                    }
                }
            }
        }
    }
};

It’s like organizing your closet: instead of one giant pile, use smaller drawers and compartments. Your future self (and your teammates!) will thank you.

Testing with Custom Types

Custom types make testing so much easier because you can create test data that matches your real data structures perfectly. No more guessing what properties your test objects need!

// Create helper functions for testing
// 'Partial<User>' makes all properties of User optional, useful for providing only necessary overrides
function createTestUser(overrides: Partial<User> = {}): User {
  return {
    id: "test-user-123",
    firstName: "Test",
    age: 25,
    emailAddress: "test@example.com",
    ...overrides // Merge any provided overrides
  };
}

// Use in tests
const youngUser = createTestUser({ age: 18 });
const oldUser = createTestUser({ age: 65 });
const userWithCustomEmail = createTestUser({ emailAddress: "custom@example.com" });

// TypeScript ensures all your test data is valid according to the User type
console.log(youngUser);
console.log(oldUser);
console.log(userWithCustomEmail);

This way, your test data is always consistent with your application’s types, making your tests more reliable and easier to write.

The Bottom Line: Why This Matters

Here’s the thing—custom types are not just about preventing errors (though they are amazing at that). They are about making your code tell a story. When someone (or even future you!) reads your code and sees a User type, they immediately understand what that data represents. When they see an Order type, they know what properties it has.

Without custom types, your code is like a conversation where everyone speaks a different language, leading to misinterpretations and frustration. With custom types, everyone’s on the same page, speaking the same language, understanding exactly what’s expected. It’s like having a universal translator built right into your code.

Plus, let’s be honest—there’s something deeply satisfying about writing code that just works. No more spending hours hunting down typos in property names. No more wondering if a function expects a string or a number. No more “it works on my machine” moments that mysteriously fail in production. It makes coding less about debugging and more about building cool stuff.

Conclusion

Start small. Pick one object in your current project—maybe a user, a product, or whatever data you are working with most. Define a custom type for it. Use it in a few places. See how it feels.

I guarantee you will have one of those “where has this been all my life?” moments. And once you do, you will wonder how you ever lived without custom types.

They are not just a TypeScript feature—they are a superpower. Use them wisely, and your code will thank you for it.

What’s the first custom type you’re going to create? Trust me, once you start, you won’t want to stop.

For more information, visit: TypeScript

Leave a Reply

Your email address will not be published. Required fields are marked *