Why TypeScript Only Lets You Touch weight
TypeScript’s type system trips up many developers when they first encounter a union of object types. Given an animal that could be a Dog or a Cat, editors only autocomplete the shared weight property — not whiskers or friendly. To understand why, we need to start with how TypeScript validates types in the first place.
Structural Typing vs. Nominal Typing
Most traditionally typed languages like C# or Java are nominally typed. In those languages, even if two classes have identical shapes, they are distinct types. This code won’t compile in C#:
class Foo {
public int x;
}
class Blah {
public int x;
}
Blah b = new Foo();
Cannot implicitly convert type 'Foo' to 'Blah'
The class name matters; structure alone isn’t enough for assignment compatibility.
TypeScript inverts this logic. It cares about shape. If a value has the same properties and types as a declared interface, it’s compatible — regardless of how it was constructed. This is structural typing.
Consequently, this runs without error:
class Foo {
x: number = 0;
}
class Blah {
x: number = 0;
}
let f: Foo = new Blah();
let b: Blah = new Foo();
A variable declared as a class instance simply expects any object matching its structure. A plain object literal works fine as long as the shape lines up:
class Foo {
x: number = 0;
}
let f: Foo;
let f: Foo;
f = {
x: 0
}
Union Types Are Sets of Values, Not Property Merges
Now back to the original problem:
interface Cat {
weight: number;
whiskers: number;
}
interface Dog {
weight: number;
friendly: boolean;
}
let animal: Dog;
Declaring animal as a Dog means it accepts any object matching the interface’s structure. Adding a union expands that:
let animal: Dog | Cat;
Now animal can be a valid Dog value or a valid Cat value.
The crucial point: TypeScript only grants access to properties it can prove exist on every possible type in the union. Since friendly only exists on Dog, and whiskers only on Cat, neither is safe to access without first narrowing the type. The compiler won’t let you risk a runtime error.
Developers often write something like this:
let animal: Dog | Cat;
…and wrongly expect animal to expose all properties from both interfaces. In reality, the union applies to values, not to the property sets. Since the value could be either shape, only the intersection of properties — here, just weight — is guaranteed present.
Narrowing with the in Operator
To safely access type-specific properties, you must narrow the union. A basic approach uses JavaScript’s in operator. TypeScript understands in and automatically narrows the type inside the conditional branches:
let o = { a: 12 };
"a" in o; // true
"x" in o; // false
let animal: Dog | Cat = {} as any;
if ("friendly" in animal) {
console.log(animal.friendly);
} else {
console.log(animal.whiskers);
}
Inside the if block, the presence of friendly tells TypeScript this is a Dog. In the else block, it infers Cat. Editors reflect this narrowing when you hover over the variable:


This works for simple examples. But relying on property existence gets fragile as types multiply and begin sharing more members.
Discriminated Unions: The Scalable Approach
For reliable narrowing across many types, we introduce a discriminated union. Each type in the union gets a literal-type property used solely to tell them apart:
interface Cat {
weight: number;
whiskers: number;
ANIMAL_TYPE: "CAT";
}
interface Dog {
weight: number;
friendly: boolean;
ANIMAL_TYPE: "DOG";
}
Note that ANIMAL_TYPE: "CAT" isn’t a regular string — it’s a literal type that only accepts exactly "CAT". The two interfaces now carry distinct, non-overlapping discriminant values.
This enables deterministic checks:
let animal: Dog | Cat = {} as any;
if (animal.ANIMAL_TYPE === "DOG") {
console.log(animal.friendly);
} else {
console.log(animal.whiskers);
}
As long as every participating type has a unique discriminant value, this pattern is impossible to break accidentally. TypeScript enforces the value at creation time, so you’ll get an error if you forget to supply it or use the wrong one:


Why This Matters
The original confusion — why only weight is accessible — stems from treating unions as merged property bags. They aren’t. A union type describes a value that fits one of its members, and TypeScript guards against unsafe property access until you prove which member you’re dealing with.
Discriminated unions solve this cleanly. The added ANIMAL_TYPE property is minor overhead relative to predictable, compiler-checked narrowing.
For deeper understanding, TypeScript’s docs on narrowing cover these patterns thoroughly, including type predicates — custom checks that let you write your own narrowing logic without relying on discriminators or the in keyword.



