Losing Interfaces & Generics
interface and generics weren't just extra syntax — they were the two biggest
structural guarantees TypeScript gave you above the level of a single variable. An
interface guaranteed an object had a specific shape everywhere it was used. A
generic guaranteed a container stayed connected to the type it held, instead of forgetting it the
moment the value went in. Plain JavaScript has no direct replacement for
either — what it has are informal conventions that approximate the same intent, enforced
by nothing but the reader's attention.
💡 The Bottom Line
Without interface, plain JS falls back on duck typing — "if it
has the properties I need, I'll use it," with no upfront guarantee those properties exist —
plus naming conventions and, if you did lesson 2's JSDoc work, a @typedef comment
documenting the intended shape. Without generics, a function or class that's supposed to work
"for any type, but stay consistent about which one" just... doesn't enforce that consistency;
it works with whatever you pass it, correctly or not. Both are real losses, not
inconveniences — nothing below fully replaces the compile-time guarantee, it only narrows how
often you'll get bitten by its absence.
What an interface guaranteed
interface User {
id: number;
name: string;
roles: string[];
}
function isAdmin(user: User): boolean {
return user.roles.includes("admin");
}
isAdmin({ id: 1, name: "Sam" });
// compile error: Property 'roles'
// is missing in type
// '{ id: number; name: string; }'
// but required in type 'User'.
function isAdmin(user) {
return user.roles.includes("admin");
}
isAdmin({ id: 1, name: "Sam" });
// no error until this line runs:
// TypeError: Cannot read properties
// of undefined (reading 'includes')
📋 Key differences
- TypeScript checked every call site against the
interfacebefore anything ran — the missingrolesfield was caught the moment you wrote the call, not the moment it executed. - Plain JS has no concept of "this object is supposed to have these fields" beyond however
you named the parameter and whatever comment you wrote nearby — the object you pass in
either happens to have
rolesor it doesn't, and JS won't tell you which until it tries to use it. - Duck typing — "if it walks like a duck and quacks like a duck" — is JavaScript's actual answer: code doesn't check an object's declared type, it just tries to use the properties it needs and fails at that exact point if they're missing.
Approximating a shape guarantee
Since there's no compiler to hold code to an interface, the closest plain-JS
equivalents are documentation plus a runtime check at the boundary — the same guard-clause idea
from the previous lesson, applied to a whole shape instead of one parameter.
/**
* @typedef {Object} User
* @property {number} id
* @property {string} name
* @property {string[]} roles
*/
/**
* @param {User} user
* @returns {boolean}
*/
function isAdmin(user) {
if (!Array.isArray(user.roles)) {
throw new TypeError("isAdmin expects a user with a roles array");
}
return user.roles.includes("admin");
}
The @typedef restores editor autocomplete and (with @ts-check) some
compile-style checking on your own code — but it does nothing for a value that arrives
at runtime from outside your control, like a parsed API response. The manual
Array.isArray guard is what actually protects that case, and it only exists because
someone wrote it.
What a generic guaranteed
class Box<T> {
constructor(private value: T) {}
get(): T {
return this.value;
}
}
const numberBox = new Box<number>(42);
const n: number = numberBox.get();
// n is guaranteed to be a number —
// the compiler tracked T all the way through
const bad: string = numberBox.get();
// compile error: Type 'number' is not
// assignable to type 'string'.
class Box {
constructor(value) {
this.value = value;
}
get() {
return this.value;
}
}
const numberBox = new Box(42);
const n = numberBox.get();
// n is just "whatever get() returns" —
// nothing tracked that it started as a number
const alsoFine = new Box("oops");
// nothing stops mixing types across
// different Box instances either
⚠️ Gotcha
The specific thing a generic bought you — "this container's output type is the same
type as whatever went in, tracked automatically" — has no plain-JS substitute at all, not
even an approximate one. A JSDoc @template tag exists and can express something
similar for editor hints, but it's rarely used in practice and doesn't carry the same weight as
real generic inference across a whole call chain. In plain JS, a "generic-shaped" class or
function like Box above simply accepts and returns whatever it's given — the
"stays consistent" part was entirely the compiler's doing, and once it's gone, consistency is
just a convention you have to maintain by discipline.
Naming conventions as a weak stand-in
Absent any tooling, plain-JS codebases often lean on naming and structure to communicate
intent that used to be a type: a function named parseUserResponse implies (but
doesn't enforce) what shape it expects and returns; a factory function returning a plain object
literal instead of a class instance is often treated, by convention, as if it "is" a certain type
even though nothing checks that. These conventions help a human reader; they do nothing at
runtime, and nothing stops the next person (or your own code six months later) from breaking
them silently.
🔧 Try It Yourself
Take the Box class above, create two instances — one holding a number, one
holding a string — and write a function that adds 1 to whatever
.get() returns. Call it with both boxes and observe what happens with the string
one (string concatenation instead of addition, or NaN, depending on how you
wrote it) — there's no error anywhere, just a wrong answer.
🙋 There Are No Dumb Questions
Q: Is duck typing just a worse version of interfaces, or is it actually a different
way of thinking about types?
A: A bit of both. Duck typing is genuinely how dynamically-typed languages have always worked,
and plenty of flexible, working software is built entirely on it — it's not automatically
wrong. But coming from TypeScript, it's worth being honest that it's strictly less safe for the
specific case of "I need to guarantee this object has these fields before I use them" — duck
typing defers that check to the exact moment of use (or never, if the code path is rarely
exercised), where an interface caught it at every call site, immediately, every
time.
✅ Quick Check
In plain JavaScript, a Box class holds any value passed to its constructor and
returns it from .get(). What enforces that a single Box instance's
output type matches what it was constructed with?
Next up: time for the recap — but first, a rundown of the specific everyday traps that catch
even developers who've internalized everything so far: silent coercion, null vs
undefined, and switch statements that used to be exhaustively checked.