Using TypeScript typeof for advance type safety
You've deployed your app, and suddenly users are getting crashes. The API response format changed, and your code assumed a userId would always be a number, but it's now coming through as a string. TypeScript didn't catch it because you typed the response as any. If only you'd extracted the type from your actual data structure...
The TypeScript typeof operator solves exactly this problem. It lets you capture types from existing values at compile time, creating a single source of truth between your runtime data and type definitions. This guide shows you practical patterns for using typeof to build more resilient, type-safe applications.
Using typeof for Type Checking and Type Guards
In TypeScript, typeof serves dual purposes: it captures a variable's type structure at compile time and acts as a type guard at runtime. TypeScript's control flow analysis recognizes typeof checks and automatically narrows types within conditional blocks.
// Compile-time: Extract type structure
const name: string = 'John';
type NameType = typeof name; // type: string
// Runtime: Type guard with automatic narrowing
function formatValue(value: string | number | boolean) {
if (typeof value === 'string') {
// TypeScript narrows value to string automatically
return value.trim().toUpperCase();
}
if (typeof value === 'number') {
// value is number here
return value.toFixed(2);
}
// value must be boolean here
return value ? 'Yes' : 'No';
}
TypeScript recognizes these typeof checks as type guards: typeof v === "typename" and typeof v !== "typename", where typename must be one of: "string", "number", "bigint", "boolean", "symbol", "undefined", "object", or "function".
Here's a real-world example validating API responses:
interface ApiResponse {
data: unknown;
error?: unknown;
}
function processApiResponse(response: ApiResponse) {
// Check if error exists and is a string
if (response.error !== undefined && typeof response.error === 'string') {
// response.error is narrowed to string
throw new Error(`API Error: ${response.error}`);
}
// Validate data is an object (not null or primitive)
if (typeof response.data !== 'object' || response.data === null) {
throw new Error('Invalid response format');
}
return response.data;
}
Inferring Variable Types with typeof
You can apply typeof to variables to infer their types, maintaining type consistency across your codebase. This is particularly useful when working with configuration objects, API responses, or any data structure where you want to ensure type safety without duplicating type definitions.
// Database connection configuration
const dbConfig = {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
database: 'production_db',
pool: {
min: 2,
max: 10,
idleTimeoutMillis: 30000
},
ssl: process.env.NODE_ENV === 'production'
};
// Extract type from the config
type DatabaseConfig = typeof dbConfig;
// Use it in functions
function connectToDatabase(config: DatabaseConfig) {
// Full type safety and autocomplete
const connectionString = `postgres://${config.host}:${config.port}/${config.database}`;
console.log(`Pool settings: min=${config.pool.min}, max=${config.pool.max}`);
return connectionString;
}
// Ensures you pass a compatible config
connectToDatabase(dbConfig); // ✓ Valid
connectToDatabase({ host: 'localhost' }); // ✗ Error: missing properties
This approach creates a single source of truth: your runtime configuration defines both the value and the type.
Differences Between typeof in TypeScript and JavaScript
Both JavaScript and TypeScript use typeof to determine a variable's type, but TypeScript extends its functionality by allowing typeof in type contexts, making it a compile-time feature. The main differences are:
| Feature | JavaScript typeof | TypeScript typeof |
|---|---|---|
| Purpose | Runtime type check | Compile-time type reference |
| Return | String ("number", "object", etc.) | Type structure |
For example, in JavaScript, typeof returns a string at runtime:
function handleValue(value: any) {
if (typeof value === 'string') {
return value.toUpperCase();
}
return String(value);
}
In TypeScript, typeof creates a static type from an existing value:
const user = {
id: 1,
name: 'Alex',
roles: ['admin', 'editor']
};
type User = typeof user;
// Results in type:
// {
// id: number;
// name: string;
// roles: string[];
// }
TypeScript typeof vs instanceof: When to Use Each
Knowing when to reach for typeof versus instanceof is crucial for effective type checking. They serve different purposes and work with different kinds of values.
| Criteria | typeof | instanceof |
|---|---|---|
| Best for | Primitive types (string, number, boolean, etc.) | Object instances and class hierarchies |
| Returns | String literal | Boolean |
| Works with | All values including undefined | Only objects (throws on primitives) |
| Checks | Value's primitive type | Prototype chain membership |
Here's when to use each:
// Use typeof for primitives
function processInput(input: string | number) {
if (typeof input === 'string') {
// TypeScript knows input is string here
return input.toLowerCase();
}
// TypeScript knows input is number here
return input.toFixed(2);
}
// Use instanceof for class instances
class ApiError extends Error {
statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
}
}
function handleError(error: Error | ApiError) {
if (error instanceof ApiError) {
// TypeScript knows about statusCode here
console.log(`API Error ${error.statusCode}: ${error.message}`);
} else {
console.log(`Error: ${error.message}`);
}
}
// typeof quirk: null is "object"
function checkValue(value: string | null) {
if (typeof value === 'object') {
// This catches null! Need additional check
return value === null ? 'null value' : 'object';
}
return value.toUpperCase();
}
// instanceof handles null gracefully
function checkInstance(value: Date | null) {
if (value instanceof Date) {
// Safe - instanceof returns false for null
return value.toISOString();
}
return 'not a date';
}
The key difference: typeof works at the JavaScript type level (primitives), while instanceof works at the object prototype level (classes and constructors).
Ensuring Type Safety for Constants with typeof
Constants often define important application settings. Using typeof with constants, especially when combined with as const, gives you precise literal types instead of general primitive types.
// Simple constants get literal types
const MAX_RETRIES = 3;
const API_VERSION = 'v2';
type MaxRetries = typeof MAX_RETRIES; // type: 3 (not number)
type ApiVersion = typeof API_VERSION; // type: "v2" (not string)
// Route definitions with as const for literal types
const ROUTES = {
home: '/',
dashboard: '/dashboard',
profile: '/profile',
settings: '/settings'
} as const;
type Route = typeof ROUTES[keyof typeof ROUTES];
// type: "/" | "/dashboard" | "/profile" | "/settings"
function navigate(route: Route) {
// Only accepts exact route strings
window.location.href = route;
}
navigate('/dashboard'); // ✓ Valid
navigate('/dashbord'); // ✗ Error: Typo caught at compile time!
// API endpoint configuration
const API_CONFIG = {
version: 'v1',
endpoints: {
users: '/api/v1/users',
posts: '/api/v1/posts',
comments: '/api/v1/comments'
},
timeout: 5000
} as const;
type ApiConfig = typeof API_CONFIG;
// All properties are readonly with exact literal types
Notice how as const makes the types more specific - instead of just string, we get the exact string literals. This helps catch typos and invalid values at compile time.
Inferring Function Return Types with typeof
Using typeof with functions is particularly useful when working with TypeScript in a database or API context. You can extract both the function signature and its return type.
// API data fetcher with inferred return type
async function fetchUserProfile(userId: string) {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
return {
id: userId,
profile: data.profile,
settings: {
theme: data.theme || 'light',
emailNotifications: data.emailNotifications ?? true,
language: data.language || 'en'
},
metadata: {
lastLogin: new Date(data.lastLogin),
accountCreated: new Date(data.created)
}
};
}
// Extract the return type (includes Promise wrapper)
type UserProfileData = ReturnType<typeof fetchUserProfile>;
// type: Promise<{ id: string; profile: any; settings: {...}; ... }>
// Get the unwrapped return type
type UnwrappedProfile = Awaited<ReturnType<typeof fetchUserProfile>>;
// type: { id: string; profile: any; settings: {...}; ... }
// Extract the function signature
type FetchProfileFn = typeof fetchUserProfile;
// Use extracted types for caching or mocking
const mockFetchProfile: FetchProfileFn = async (userId: string) => {
// Implementation must match exact signature
return {
id: userId,
profile: { /* mock data */ },
settings: { theme: 'dark', emailNotifications: true, language: 'en' },
metadata: { lastLogin: new Date(), accountCreated: new Date() }
};
};
// Type-safe data handling
function cacheUserProfile(data: UnwrappedProfile) {
// TypeScript knows exact shape
localStorage.setItem(`profile-${data.id}`, JSON.stringify(data));
}
Advanced Patterns: Combining typeof with keyof and Indexed Access
When you combine typeof with keyof and indexed access types, you unlock powerful type manipulation patterns. This is especially useful for creating type-safe helpers and extracting nested types.
// Extract property value types
const apiConfig = {
baseUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
endpoints: {
users: '/users',
posts: '/posts'
}
} as const;
// Get union of all property types
type ConfigValue = typeof apiConfig[keyof typeof apiConfig];
// type: "https://api.example.com" | 5000 | 3 | { readonly users: "/users"; readonly posts: "/posts"; }
// Get specific nested type
type Endpoints = typeof apiConfig['endpoints'];
// type: { readonly users: "/users"; readonly posts: "/posts"; }
// Get union of endpoint paths
type EndpointPath = typeof apiConfig['endpoints'][keyof typeof apiConfig['endpoints']];
// type: "/users" | "/posts"
// Type-safe property accessor
function getConfigValue<K extends keyof typeof apiConfig>(
key: K
): typeof apiConfig[K] {
return apiConfig[key];
}
const timeout = getConfigValue('timeout'); // type: 5000
const endpoints = getConfigValue('endpoints'); // type: { readonly users: "/users"; ... }
This pattern is particularly powerful when working with configuration objects or data structures where you want to maintain type safety while accessing nested properties:
// Environment configuration with type extraction
const envConfig = {
development: {
apiUrl: 'http://localhost:3000',
debug: true
},
production: {
apiUrl: 'https://api.production.com',
debug: false
}
} as const;
type Environment = keyof typeof envConfig; // "development" | "production"
type EnvConfig = typeof envConfig[Environment]; // Union of both config types
// Type-safe environment getter
function getEnvConfig<E extends Environment>(env: E): typeof envConfig[E] {
return envConfig[env];
}
const prodConfig = getEnvConfig('production');
// Type knows: { readonly apiUrl: "https://api.production.com"; readonly debug: false }
Combining typeof with Other TypeScript Utility Types
Type narrowing becomes more powerful when you combine typeof with utility types like Partial or Readonly to boost type safety. For example:
// Start with a configuration object
const defaultSettings = {
theme: 'light',
fontSize: 16,
notifications: {
email: true,
push: false,
frequency: 'daily'
}
};
// Make all fields optional
type PartialSettings = Partial<typeof defaultSettings>;
// Make all fields readonly
type ReadonlySettings = Readonly<typeof defaultSettings>;
// Pick specific fields
type ThemeSettings = Pick<typeof defaultSettings, 'theme' | 'fontSize'>;
// Create a type for updates
function updateSettings(updates: PartialSettings) {
// Type-safe partial updates
}
// Use in practice
updateSettings({ fontSize: 20 }); // Valid
updateSettings({ notifications: { email: false } }); // Valid
updateSettings({ invalid: true }); // Error: invalid property
Common Errors and typeof Quirks
Understanding typeof's limitations and JavaScript's quirks helps you avoid common pitfalls and write more robust type guards.
Common Mistakes
// Mistake 1: Using typeof with type imports
import { UserType } from './types';
type NewType = typeof UserType; // Error: 'UserType' refers to a type, but is used as a value
// Fix: Use the type directly
import type { UserType } from './types';
type NewType = UserType;
// Mistake 2: typeof with generic functions
function getValue<T>(value: T) {
return value;
}
type ValueType = typeof getValue; // Gets function type, not the generic return type
// Fix: Use ReturnType and specify the generic
type StringValue = ReturnType<typeof getValue<string>>;
// Mistake 3: Using typeof on non-values
type Status = 'active' | 'inactive';
type StatusType = typeof Status; // Error: 'Status' refers to a type, but is used as a value
// Fix: Define a const value if you need typeof
const statusValues = ['active', 'inactive'] as const;
type Status = typeof statusValues[number];
typeof Quirks and Edge Cases
JavaScript's typeof has some surprising behaviors you need to handle:
// Quirk 1: typeof null === "object"
function processValue(value: string | null) {
if (typeof value === 'object') {
// This catches null! Always check for null explicitly
if (value === null) {
return 'null value';
}
// Won't reach here since value is never an object in this union
}
return value.toUpperCase();
}
// Better approach: Check for null first
function processValueSafe(value: string | null) {
if (value === null) {
return 'null value';
}
// value is narrowed to string here
return value.toUpperCase();
}
// Quirk 2: typeof [] === "object" and typeof {} === "object"
function handleData(data: unknown) {
if (typeof data === 'object' && data !== null) {
// Could be array, plain object, Date, RegExp, etc.
// Need more specific checks
if (Array.isArray(data)) {
return `Array with ${data.length} items`;
}
return 'Some object';
}
return 'Not an object';
}
// Quirk 3: typeof NaN === "number"
function validateNumber(input: unknown): number {
if (typeof input === 'number') {
// Still need to check for NaN!
if (Number.isNaN(input)) {
throw new Error('NaN is not a valid number');
}
return input;
}
throw new Error('Not a number');
}
// Quirk 4: Arrays and typeof
const items = [1, 2, 3];
type ItemsType = typeof items; // type: number[] ✓
typeof items; // "object" at runtime (not "array")
// Use Array.isArray for runtime array checks
function processItems(data: unknown) {
if (typeof data === 'object' && Array.isArray(data)) {
// TypeScript narrows to unknown[]
return data.length;
}
return 0;
}
These quirks are JavaScript behaviors, not TypeScript bugs. Always combine typeof with additional runtime checks when dealing with null, arrays, or special number values like NaN.
Final Thoughts on TypeScript typeof
The typeof operator bridges TypeScript's compile-time type system with JavaScript's runtime behavior. Here's what you should remember:
Use typeof for:
- Extracting types from runtime values to create a single source of truth
- Building type guards that work with TypeScript's control flow analysis
- Checking primitive types at runtime (
string,number,boolean, etc.) - Combining with
keyofand indexed access for advanced type patterns
Avoid common pitfalls:
- Always check for
nullexplicitly (sincetypeof null === "object") - Use
Array.isArray()for array checks (nottypeof) - Remember
typeofonly works on values, not types or interfaces - Watch out for
NaNwhen checking numbers
When you combine typeof with other TypeScript features like utility types, as const, and generics, you get powerful type safety without sacrificing the flexibility that makes TypeScript great.
For more advanced TypeScript patterns, check out our guides on discriminated unions, keyof operator, and type assertions.