Skip to main content

TS Ep 28: The `typeof` Type Operator (Value to Type Space)

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 28: This Article
In JavaScript, typeof is a runtime operator returning string names of primitives ("string", "object"). In TypeScript, typeof used in Type Space queries the exact compile-time shape of any JavaScript variable, object, or function declaration.

1. Value Space vs. Type Space
#

To use TypeScript effectively, you must mentally separate your code into two distinct worlds:

  1. Value Space (Runtime): Contains executable JavaScript code—variables, functions, objects, classes, and runtime typeof expressions.
  2. Type Space (Compile-Time): Contains TypeScript type definitions—interface, type, keyof, type annotations, and type-level typeof expressions. All of Type Space is completely erased during compilation.
const appSettings = {
  theme: "dark",
  maxUploadMb: 50,
  features: ["auth", "payments"],
};

// 1. Value Space (Runtime JS typeof check):
if (typeof appSettings.maxUploadMb === "number") {
  console.log("Valid upload limit.");
}

// 2. Type Space (Compile-Time TS typeof query):
type AppSettings = typeof appSettings;
/* Inferred Type:
{
  theme: string;
  maxUploadMb: number;
  features: string[];
}
*/

2. Reverse-Engineering Types from Runtime Objects
#

Instead of creating a complex interface manually and trying to keep it in sync with a default config object, use typeof to derive the type directly from the object value:

// Single Source of Truth Runtime Config Object
export const defaultDbConfig = {
  host: "localhost",
  port: 5432,
  database: "app_db",
  pool: {
    min: 2,
    max: 10,
  },
  ssl: false,
};

// Derive the TypeScript interface directly from the runtime object:
export type DbConfig = typeof defaultDbConfig;

// Use derived type in factory functions:
function initializePool(config: DbConfig) {
  console.log(`Connecting to ${config.host}:${config.port}/${config.database}`);
}

initializePool(defaultDbConfig); // 🟢 WORKED!

3. Extracting Function Signatures
#

You can use typeof to capture the complete parameter and return signature of a function declaration:

function calculateOrderTotal(subtotal: number, taxRate: number, discountCode?: string): number {
  const discount = discountCode === "PROMO10" ? 10 : 0;
  return (subtotal - discount) * (1 + taxRate);
}

// 🟢 Extract the function signature type:
type OrderCalculatorFn = typeof calculateOrderTotal;
// Inferred Type: (subtotal: number, taxRate: number, discountCode?: string) => number

// Apply signature to a mock function:
const mockCalculator: OrderCalculatorFn = (sub, tax, disc) => 100;

4. Combining typeof with Utility Types (ReturnType & Parameters)
#

By combining typeof with TypeScript’s built-in utility types, you can extract individual components of a function signature:

function buildUserSession(userId: string, role: "admin" | "user") {
  return {
    sessionId: `sess_${userId}_${Date.now()}`,
    user: { id: userId, role },
    expiresAt: new Date(Date.now() + 3600000),
  };
}

// 1. Extract Return Type from function value:
type UserSession = ReturnType<typeof buildUserSession>;
/* Inferred Type:
{
  sessionId: string;
  user: { id: string; role: "admin" | "user" };
  expiresAt: Date;
}
*/

// 2. Extract Parameter Tuple from function value:
type SessionParams = Parameters<typeof buildUserSession>;
// Inferred Type: [userId: string, role: "admin" | "user"]

5. The Ultimate Combo: keyof typeof
#

The keyof typeof pattern is the standard TypeScript design pattern for creating strict string literal unions from runtime dictionary objects or as const config maps.

// 1. Define runtime object dictionary with 'as const'
export const API_ENDPOINTS = {
  AUTH_LOGIN: "/api/v1/auth/login",
  USER_PROFILE: "/api/v1/user/profile",
  PAYMENT_CHECKOUT: "/api/v1/payment/checkout",
} as const;

// 2. Extract key union using 'keyof typeof':
export type ApiEndpointKey = keyof typeof API_ENDPOINTS;
// Inferred Type: "AUTH_LOGIN" | "USER_PROFILE" | "PAYMENT_CHECKOUT"

// 3. Extract value union using 'typeof OBJ[keyof typeof OBJ]':
export type ApiEndpointUrl = typeof API_ENDPOINTS[keyof typeof API_ENDPOINTS];
// Inferred Type: "/api/v1/auth/login" | "/api/v1/user/profile" | "/api/v1/payment/checkout"

// 4. Implement type-safe route fetcher:
function callApi(endpointKey: ApiEndpointKey) {
  const url: ApiEndpointUrl = API_ENDPOINTS[endpointKey];
  console.log(`FETCH -> ${url}`);
}

callApi("AUTH_LOGIN"); // 🟢 Output: "FETCH -> /api/v1/auth/login"

// ❌ Compiler Error: Argument of type '"INVALID_KEY"' is not assignable to parameter of type 'ApiEndpointKey'.
// callApi("INVALID_KEY");

Summary & Next Steps
#

In this episode:

  • We separated Value Space (runtime JS expressions) from Type Space (compile-time TS declarations).
  • We reverse-engineered object types from runtime variables (type Config = typeof defaultConfig).
  • We extracted function signatures, ReturnType<typeof fn>, and Parameters<typeof fn>.
  • We mastered the keyof typeof pattern for as const configuration dictionaries.

In Episode 29: Introduction to Generics, we will step into TypeScript’s most powerful feature: passing types as variables using Generics (<T>)!

typescript - This article is part of a series.
Part 28: This Article