Skip to content

The in Operator and Property Checks

typeof narrows primitives. instanceof narrows class instances. For plain objects — especially data from external sources like APIs or localStorage — neither tool fits. The in operator fills that gap: it checks whether a property exists on an object and narrows the type accordingly.

'property' in obj returns true at runtime if obj has a property named 'property', either directly or in its prototype chain. TypeScript uses this check to narrow a union:

interface Cat { meow(): void }
interface Dog { bark(): void }
function makeSound(animal: Cat | Dog): void {
if ('meow' in animal) {
animal.meow(); // TypeScript knows: animal is Cat
} else {
animal.bark(); // TypeScript knows: animal is Dog
}
}

Inside the 'meow' in animal branch, TypeScript narrows animal to Cat because only Cat declares meow. In the else branch, only Dog remains.

The in operator is the right choice when the types in a union do not share a common discriminant property. When a discriminant is available — a shared property with a different literal value in each member — a discriminated union (next lesson) is cleaner. Use in for:

  • Objects whose types were not designed to share a discriminant.
  • Validating unknown data from external sources.
  • Checking for optional properties before use.

The most common place in appears in real code is at runtime data boundaries — API responses, localStorage reads, JSON.parse results — where the actual shape is unknown and must be verified before use.

AceIt’s loadHighScore reads from localStorage and casts to HighScore:

static loadHighScore(): HighScore | null {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
try {
return JSON.parse(raw) as HighScore;
} catch {
return null;
}
}

The as HighScore cast is a trust assertion — if the stored data is corrupted or from an older version of the app, the cast succeeds but the resulting object is not actually a HighScore. The in operator lets you check before trusting:

function isHighScoreShape(value: unknown): boolean {
return (
typeof value === 'object' &&
value !== null &&
'score' in value &&
'total' in value &&
'date' in value
);
}
static loadHighScore(): HighScore | null {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
try {
const parsed: unknown = JSON.parse(raw);
if (!isHighScoreShape(parsed)) return null;
return parsed as HighScore;
} catch {
return null;
}
}

Now the cast only happens after confirming the required properties exist. This does not verify the types of those properties — for that you need a full type predicate (next lesson) — but it eliminates the most common failure mode.

in also narrows within a single type when a property is optional:

interface SearchOptions {
amount: number;
difficulty?: Difficulty;
}
function logOptions(options: SearchOptions): void {
if ('difficulty' in options) {
console.log(options.difficulty); // TypeScript knows: difficulty is Difficulty (not undefined)
}
}

When difficulty is optional (Difficulty | undefined), TypeScript inside the 'difficulty' in options branch narrows it to Difficulty — the presence check rules out undefined.

Real validation combines both tools:

function isStringRecord(value: unknown): boolean {
return (
typeof value === 'object' && // rules out primitives
value !== null && // rules out null (typeof null === 'object')
'key' in value // checks for the specific property
);
}

The typeof === 'object' check must come before 'key' in value because in only works on objects — applying it to a primitive throws at runtime. TypeScript enforces this ordering: it will report an error if you use in on a type that includes non-objects without first narrowing.

In a scratch file validation.ts:

  1. Write a union type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number }. Use 'radius' in shape to narrow — observe that TypeScript also considers kind in the result. Then switch to a discriminated union approach (using shape.kind) and compare which is cleaner.
  2. Write function hasRequiredFields(value: unknown): boolean that checks whether value is a non-null object with score, total, and date properties — using the pattern from isHighScoreShape above.
  3. Note the hasRequiredFields pattern — you will apply it inside isHighScore in quiz.ts when you write the full QuizEngine class in Module 07.
  • 'property' in obj returns true if the property exists on the object or its prototype chain — TypeScript narrows the type inside the branch.
  • Use in when types in a union do not share a discriminant property.
  • Always pair in with typeof === 'object' and a !== null check when the value could be a primitive or null.
  • in is the primary tool for validating parsed external data before casting.
  • For shared discriminant properties, a discriminated union (next lesson) is cleaner than in.