Before You Write Another Line of Code
You’re here because someone threw TypeScript into your React project and now you're knee-deep in red squiggly lines. And then you saw something like interface Foo and type Foo and thought, what’s the damn difference?
This article will set that straight. Just real answers, best practices. You’ll know when to use what, why, and how.
First, What’s the Deal?
Both interface and type let you define the shape of data. like blueprints for your props, state, context, and all the stuff you pass around in React.
interface User {
id: number;
name: string;
}
type Product = {
id: number;
price: number;
}
Looks the same? It mostly is. Until it’s not.
Let’s break it down.
Interfaces: Blueprint Definitions Rooted in Object-Oriented Programming
Interfaces are extendable and play nicely with object-oriented patterns. You’ll want these when you’re building components or models that can be built upon.
// Creating an Employee object
const developer : Employee = {
name: "Alex",
role: "SDE"
}
// Printing an Employee object
function printEmployeeDetails(emp: Employee){
console.log(`${emp.name} is a ${emp.role}`);
}
printEmployeeDetails(developer);// Output: Alex is a SDE
// Creating an Employee array
const team : Employee[] = [
{ name: "Alex", role: "SDE"},
{ name: "Martin", role: "QA"}
]
team.forEach(printEmployeeDetails);// Output: Alex is a SDE, Martin is a QA
Function that accepts both Person and Employee
In the following example, I am passing Employee as an argument, but the funtion onboardNewEmployee() accepts Person as a parameter. However, it still works.
Why?
- We're only accessing the name, which is declared on the Person interface.
- TypeScript sees this as valid because it's fully within the bounds of the Person type.
function onboardNewEmployee(person: Person){// Accepting Person as parameter
console.log(`${person.name}`);
}
const developer : Employee = {
name: "Alex",
role: "SDE"
}
onboardNewEmployee(developer); // Passing employee as an argument
Now, say I change my method body and want to print the role too.
Now, TS gives me an ERROR: Property 'role' does not exist on type 'Person'.
Why?
- The parameter is typed as Person, which only has one property: name: string.
- TypeScript enforces type safety and doesn’t allow access to properties (role) that aren’t declared in the type.
function onboardNewEmployee(person: Person){
console.log(`${person.name} is a ${person.role}`); //ERROR: Property 'role' does not exist on type 'Person'.
}
How do you fix this? Simple use "in" operator.
- 'role' in person checks whether the person object has a role property.
- TypeScript now understands: “Ah! This must be an Employee, not just a Person.”
- You can now safely access the person.role, either directly (if TypeScript is smart enough to infer it), or by asserting: (person as Employee).
function onboardNewEmployee2(person: Person){
if('role' in person){
console.log(`${person.name} is a ${person.role}`); // Alex is a SDE
}
else
{
console.log(`${person.name}`);// King Julien
}
}
interface Person {
name: string;
}
// Creating a Person object
const kj : Person = {
name: "King Julien"
}
interface Employee extends Person {
role: string;
}
// Creating an Employee object
const developer : Employee = {
name: "Alex",
role: "SDE"
}
onboardNewEmployee2(kj);
onboardNewEmployee2(developer);
Why Use in?
Because TypeScript is structurally typed, it doesn't know what subtype an object is just by looking at its declared type. The in operator helps narrow it down.
Do
- Use interfaces when you're dealing with object shapes.
- Need to extend other interfaces.
- Use them when you expect others (or yourself) to build on them.
Don’t
- Use interfaces for union types. It won’t work. Period.
Types: The Swiss Army Knife
Tyes are more flexible. They can do everything from union types to conditional types. You wanna define something that’s not just an object? You need type.
type Status = "Success" | "Error" | "Loading";
type Request = {
url: string;
status: Status;
}
const apiCall1: Request = {
url: 'https://api.example.com/data',
status: "Loading",
}
const apiCall2: Request = {
url: 'https://api.example.com/data',
status: "Success",
}
function getStatus(status: Status) {
switch (status) {
case "Success":
return "All done!";
case "Loading":
return "Loading, please wait...";
case "Error":
return "Something went wrong!";
}
}
getStatus("Success");
Tuple Type
type Point = [number, number];
const start: Point = [0, 0];
const end: Point = [10, 10];
Useful for [x, y], [start, end], etc. Again: the interface has no idea what to do here.
Function Signatures
type clickHandler = (event : React.MouseEvent<HTMLButtonElement>) => void;
const handleClick: clickHandler = (e) => {
console.log("clicked:", e.altKey);
}
Perfect for typing callbacks, utils, custom hooks, whatever. You can’t define a function shape this cleanly with the interface.
Intersection Types
type A = { foo : string }
type B = { bar : number }
type C = A & B;
const value: C = {
foo: "Hi",
bar : 1000
}
console.log(value.foo);
console.log(value.bar);
Type C has both the properties of A and B. This is powerful when you’re composing types across modules or features.
You can’t do this kind of mix-and-match merging with the interface as flexibly. This is where type can come in handy for intersecting and building complex shapes.
When They Clash: Interface vs Type
Here’s what nobody tells you: In 99% of real-world apps, you can use either one. But here’s what’ll make you look like you know what you’re doing:
Use Case |
Go With |
Extending objects |
Interface |
React props |
Interface |
Complex type operations |
Type |
Unions and intersections |
Type |
Real-Life React Example
1. Interface for Component Props
interface ButtonProps {
label : string;
onClick: () => void;
disabled? : boolean;
}
const Button = ({label, onClick, disabled}: ButtonProps) => (
<button onClick={onClick} disabled={disabled}>{label}</button>
);
2. Type for State
type Theme = "Light" | "Dark" | "Blue";
const [theme, setTheme] = useState<Theme>("Light");
3. Advanced Tip: You Can Combine Them
interface Base{
id: number
}
type Extended = Base & {
name: string;
}
const character : Extended = {
id: 1,
name: "Alex",
}
You get the inheritance of interface with the flexibility of type. Use this when you want to extend an object and mix in some unions or function types.
Best Practices (Write These Down)
- Props? Interface. Keeps things clean.
- Union of values? Type.
- Want to extend? Use the interface unless you need a union.
- Don't convert between them unless there's a clear reason. Stick to one style in a module.
- When in doubt, pick the one that makes your type readable.
The Takeaway
Stop wasting time wondering which is better. They’re tools. Use them like tools. Screwdrivers don’t replace hammers.
You’re writing real apps, not textbooks. Keep your types sharp, your props clean, and your code readable for that poor soul (probably future-you) who has to fix it next week.