Question
Given a TypeScript variable that can hold either a number or a string:
let abc: number | string;
How can you determine whether its current value is a number or a string? Is there an equivalent to checking abc.type === "number"?
if (abc.type === "number") {
// Do something
}
Short Answer
You will learn why TypeScript type annotations are not available at runtime, how to use JavaScript's typeof operator to inspect primitive values, and how TypeScript narrows a union type safely inside a conditional branch.
Concept
TypeScript lets you describe a value using types:
let abc: number | string;
This is a union type. It means abc may contain a number or a string.
However, number and string in this declaration are TypeScript compile-time types. They help the editor and compiler detect mistakes, but they are removed when TypeScript is compiled to JavaScript. Therefore, a normal primitive value does not have a .type property.
Use JavaScript's typeof operator to inspect primitive values at runtime:
if (typeof abc === "number") {
// Here, TypeScript knows abc is a number.
}
This process is called type narrowing. Before the check, abc is number | string. In the if branch, TypeScript narrows it to number; in the else branch, it narrows it to string.
Type narrowing matters because number methods and string methods are different. You should check which kind of value you have before using operations that only one type supports.
Mental Model
Think of abc as a box that is allowed to hold one of two kinds of items: a number card or a text card.
The annotation number | string is a rule written on the box for TypeScript's compiler. It is not an object or label stored inside the box at runtime.
typeof abc is like looking at the item currently inside the box. Once you see it is a number card, TypeScript lets you use number-specific operations. If it is text, you can use string-specific operations.
Syntax and Examples
The syntax for checking a primitive value is:
typeof value === "typeName"
For a number | string union:
function formatValue(value: number | string): string {
if (typeof value === "number") {
return `Doubled: ${value * 2}`;
}
return `Uppercase: ${value.toUpperCase()}`;
}
console.log(formatValue(21)); // Doubled: 42
console.log(formatValue("hello")); // Uppercase: HELLO
Inside this condition:
if (typeof value === "number") {
TypeScript treats value as a number, so value * 2 is valid. After the early return, only the string case remains, so value.toUpperCase() is valid.
Common typeof result strings include:
typeof 42; // "number"
typeof "hello"; // "string"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof 10n; // "bigint"
typeof Symbol("x"); // "symbol"
typeof (() => {}); // "function"
typeof {}; // "object"
Step by Step Execution
Consider this function:
function describe(abc: number | string): string {
if (typeof abc === "number") {
return `The value is ${abc.toFixed(2)}`;
}
return `The text has ${abc.length} characters`;
}
console.log(describe(4));
console.log(describe("TypeScript"));
Execution for describe(4):
abcreceives4.typeof abcevaluates to"number".- The condition is true.
- TypeScript has narrowed
abctonumberin this block. abc.toFixed(2)returns"4.00".
Execution for describe("TypeScript"):
abcreceives the string"TypeScript".typeof abcevaluates to"string".- The condition is false, so execution continues after the
ifblock. - TypeScript knows the remaining possible type is
string. abc.lengthreturns10.
Real World Use Cases
Runtime type checks are useful when a value can arrive in more than one form:
- API responses: An API field may return an ID as either
123or"123". - Form inputs: Browser form values are usually strings, while application code may also receive numeric values programmatically.
- Configuration: A setting might accept
true,false, or a string such as"auto". - Reusable utilities: A formatter may accept a date timestamp as a number or a preformatted string.
- Command-line arguments: Parsed options may need conversion or validation before use.
Example: normalize an ID before sending it to a database query.
function normalizeId(id: number | string): string {
if (typeof id === "number") {
return id.toString();
}
return id.trim();
}
Real Codebase Usage
In production code, developers often pair narrowing with validation and early returns.
Guard clause
A guard clause handles an unsupported value immediately, keeping the main logic simple:
function calculateDiscount(value: number | string): number {
if (typeof value !== "number") {
throw new TypeError("Discount must be a number");
}
return value * 0.1;
}
Normalizing values at a boundary
Check values near the point where they enter your application, then return one predictable type:
function parsePage(value: number | string): number {
if (typeof value === "number") {
return value;
}
const page = Number(value);
return Number.isInteger(page) && page > 0 ? page : 1;
}
Custom type guard for reusable checks
When a check is used in several places, create a function whose return type tells TypeScript what it proves:
function isNumber(value: unknown): value is number {
return typeof value === "number";
}
function addTax(value: unknown): number {
if (!isNumber(value)) {
throw new TypeError("Expected a number");
}
return value * 1.2;
}
Use unknown for untrusted input. Unlike any, it requires you to validate the value before using it.
Common Mistakes
Using .type on a primitive
This does not check a TypeScript type:
let abc: number | string = 5;
if (abc.type === "number") {
// Error: Property 'type' does not exist on type 'number'.
}
Use typeof abc === "number" instead.
Forgetting quotes around the type name
This is incorrect because number is not the string returned by typeof:
if (typeof abc === number) {
// Incorrect
}
Write:
if (typeof abc === "number") {
// Correct
}
Using typeof to distinguish arrays from objects
Arrays are objects at runtime:
typeof []; // "object"
Use Array.isArray(value) for arrays.
Expecting typeof null to return "null"
This is a long-standing JavaScript behavior:
typeof null; // "object"
Check it directly:
if (value === null) {
// value is null
}
Declaring a variable without assigning it
With strictPropertyInitialization and strict checking, reading an unassigned local variable is an error:
let abc: number | string;
console.log(typeof abc); // Error: variable used before being assigned
Assign it first, receive it as a function parameter, or include undefined if that is genuinely possible:
let abc: number | string | undefined;
Comparisons
| Situation | Recommended check | Why |
|---|---|---|
number, string, boolean, undefined, bigint, symbol, or function | typeof value === "number" | typeof is designed for primitive runtime categories. |
| Array | Array.isArray(value) | typeof [] is "object". |
| Instance of a class | value instanceof MyClass | Checks whether the object comes from that class's prototype chain. |
| Object with a distinguishing field | "kind" in value or value.kind === "..." | Useful for object unions with a discriminant property. |
null | value === null | typeof null is incorrectly "object" in JavaScript. |
| Compile-time-only TypeScript type or interface | Validate its shape at runtime | Interfaces and type aliases do not exist in emitted JavaScript. |
For object unions, add your own property rather than expecting .type to appear automatically:
type Result =
| { kind: "success"; value: number }
| { kind: "error"; message: string };
function handle(result: Result): string {
if (result.kind === "success") {
return result.value.toFixed(2);
}
return result.message;
}
Cheat Sheet
// Primitive type checks
if (typeof value === "number") {}
if (typeof value === "string") {}
if (typeof value === "boolean") {}
if (typeof value === "undefined") {}
if (typeof value === "function") {}
// A number-or-string union
function print(value: number | string) {
if (typeof value === "number") {
return value.toFixed(2); // value: number
}
return value.toUpperCase(); // value: string
}
// Special cases
Array.isArray(value); // arrays
value === null; // null
value instanceof Date; // class instances
- TypeScript annotations are checked during compilation, not stored at runtime.
- Use the exact lowercase strings returned by
typeof, such as"number"and"string". typeof nullis"object"; compare withnulldirectly.typeofcannot verify a TypeScript interface or type alias at runtime.- Prefer
unknownoveranyfor data that must be validated.
FAQ
How do I check if a variable is a number in TypeScript?
Use typeof value === "number".
if (typeof value === "number") {
console.log(value.toFixed(2));
}
Can I use variable.type to get a TypeScript type?
No. Primitive values do not automatically have a .type property, and TypeScript types are removed when code is compiled to JavaScript.
Does typeof narrow union types in TypeScript?
Yes. In a number | string union, checking typeof value === "number" narrows the value to number in that branch.
Why is typeof null equal to "object"?
It is a historical JavaScript quirk. Use value === null when you need to detect null.
How can I check whether a value is an array?
Use Array.isArray(value), not typeof value === "array".
if (Array.isArray(value)) {
console.log(value.length);
}
Can typeof check a TypeScript interface?
No. Interfaces do not exist at runtime. Check required object properties, use a custom type guard, or use a validation library when validating external data.
Should I use any when a value may have several types?
Usually no. Use a union such as number | string when the possible types are known, or unknown for untrusted values that must be checked first.
Mini Project
Description
Build a small formatter that accepts a product price supplied either as a numeric amount or as a text value. This resembles values received from an API, form field, or configuration file. The project demonstrates checking a union type and converting both forms into one consistent output.
Goal
Create a function that safely formats a number | string price as a two-decimal currency string.
Requirements
Accept a value typed as number | string.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Accessing Input Value from EventTarget in TypeScript
Learn why EventTarget has no value property in TypeScript and safely read values from HTML input events in Angular applications.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.