function formatValue(input: string | number | boolean): string {
if (typeof input === "string") {
return input.toUpperCase();
}
if (typeof input === "number") {
return input.toFixed(2);
}
return input ? "Yes" : "No";
}
output
// TypeScript narrows the type inside each branch automatically
Note typeof works for: "string", "number", "boolean", "undefined", "symbol", "bigint", "function", "object". Note: typeof null === "object" — this is a famous JavaScript quirk that TypeScript inherits.
typeoftype guardcheck type at runtimenarrow typetypeof narrowing
// instanceof narrows to the specific class, enabling access to class-specific properties
Note instanceof checks the prototype chain at runtime. It does not work with interfaces or type aliases (they have no runtime presence). It also fails across different realms (iframes) since each realm has its own constructor.
instanceofclass type guardcheck class typeerror handling narrowing
// 'in' narrows based on the presence of a property
Note The 'in' operator checks if a property name exists on an object at runtime. TypeScript uses this to narrow union types. The property name must be a string literal for narrowing to work. Works well when union members have distinct unique properties.
in operatorproperty checknarrow by propertyhas property guard
Custom Type Guard Functions
syntax
function isType(value: ParamType): value is TargetType {
return /* boolean check */;
}
example
interface Fish {
swim: () => void;
habitat: "water";
}
interface Bird {
fly: () => void;
habitat: "air";
}
function isFish(creature: Fish | Bird): creature is Fish {
return creature.habitat === "water";
}
function move(creature: Fish | Bird) {
if (isFish(creature)) {
creature.swim(); // TypeScript knows this is Fish
} else {
creature.fly(); // TypeScript knows this is Bird
}
}
output
// Custom predicates with 'is' return type enable reusable narrowing
Note The 'value is Type' return annotation is a type predicate — it tells TypeScript to narrow the variable when the function returns true. The compiler trusts your implementation; if your check is wrong, you get silent type errors at runtime. Always keep the check accurate.
custom type guardtype predicateis keyworduser defined guardreusable narrowing
Discriminated Unions
syntax
interface A { kind: "a"; ... }
interface B { kind: "b"; ... }
type Union = A | B;
example
interface LoadingState {
status: "loading";
}
interface SuccessState {
status: "success";
data: string[];
}
interface ErrorState {
status: "error";
errorMessage: string;
}
type RequestState = LoadingState | SuccessState | ErrorState;
function renderState(state: RequestState): string {
switch (state.status) {
case"loading":
return"Loading...";
case"success":
return`Got ${state.data.length} items`; // data is availablecase"error":
return`Error: ${state.errorMessage}`; // errorMessage is available
}
}
output
// Each case automatically narrows to the correct interface
Note A discriminated union has a common literal-typed property (the 'discriminant') shared across all members. TypeScript narrows exhaustively in switch/if on that property. This is the recommended pattern for modeling states, events, and message types.
function assert(value: unknown): asserts value is Type {
if (!check) thrownew Error(...);
}
example
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
thrownew TypeError(`Expected string, got ${typeof value}`);
}
}
function assertDefined<T>(value: T | null | undefined): asserts value is T {
if (value == null) {
thrownew Error("Value must not be null or undefined");
}
}
function processInput(data: unknown) {
assertIsString(data);
// After this line, data is typed as string
console.log(data.toUpperCase());
}
output
// After the assertion call, TypeScript narrows for the rest of the scope
Note Assertion functions narrow by throwing on failure rather than returning a boolean. They use 'asserts value is Type' (or just 'asserts value' for truthiness). Unlike type predicates, they affect the current scope rather than an if branch. Must throw — not return false.
assertion functionasserts keywordthrow to narrowruntime assertionassert type
Truthiness Narrowing
syntax
if (value) { ... } // narrows out null, undefined, 0, "", false
example
function printLength(input: string | null | undefined) {
if (input) {
// input is narrowed to string (null and undefined excluded)
console.log(`Length: ${input.length}`);
} else {
console.log("No input provided");
}
}
// Combining with logical operatorsfunction getDisplayName(first?: string, last?: string): string {
return (first && last) ? `${first} ${last}` : first ?? last ?? "Anonymous";
}
output
// Truthiness checks exclude null, undefined, and falsy values
Note Truthiness narrowing also excludes 0, empty string, and false — which may not be what you want. If 0 or "" are valid values, check explicitly for null/undefined instead: if (value != null) or if (value !== undefined).