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.
The in operator
Section titled “The in operator”'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.
Narrowing with in vs discriminated unions
Section titled “Narrowing with in vs discriminated unions”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.
Validating parsed data
Section titled “Validating parsed data”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.
Narrowing optional properties
Section titled “Narrowing optional properties”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.
Combining typeof and in
Section titled “Combining typeof and in”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.
Exercise
Section titled “Exercise”In a scratch file validation.ts:
- Write a union
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number }. Use'radius' in shapeto narrow — observe that TypeScript also considerskindin the result. Then switch to a discriminated union approach (usingshape.kind) and compare which is cleaner. - Write
function hasRequiredFields(value: unknown): booleanthat checks whethervalueis a non-null object withscore,total, anddateproperties — using the pattern fromisHighScoreShapeabove. - Note the
hasRequiredFieldspattern — you will apply it insideisHighScoreinquiz.tswhen you write the fullQuizEngineclass in Module 07.
'property' in objreturnstrueif the property exists on the object or its prototype chain — TypeScript narrows the type inside the branch.- Use
inwhen types in a union do not share a discriminant property. - Always pair
inwithtypeof === 'object'and a!== nullcheck when the value could be a primitive ornull. inis the primary tool for validating parsed external data before casting.- For shared discriminant properties, a discriminated union (next lesson) is cleaner than
in.