Two worlds: values and types

A TypeScript file contains two kinds of names. Values (variables, functions, objects, classes) exist when the program runs. Types (interfaces, type aliases, type parameters) exist only for the type checker and are erased by the compiler. A few declarations create both a value and a type with the same name: a class, an enum, or the as const object plus type alias pattern.

The operators on this page all work in the type world. They take a type, or the type of a value, and compute a new type from it. That lets you write a shape once and derive the rest, instead of copying a list of names into several places that must then be kept in sync by hand.

On the left, the value world, which exists at run time: the object config. On the right, the type world, erased after compiling. The typeof operator takes the value config into the type world, giving the object type with region as string and vatRate as number. The keyof operator turns that type into the union of its keys, region or vatRate. No operator leads back from types to values. Values · exist at run time Types · erased after compiling const config = { region: "NL", vatRate: 0.21 } typeof typeof config { region: string; vatRate: … } keyof keyof typeof config "region" | "vatRate" Nothing leads back: at run time only the left side exists.
typeof crosses from a value to its type; keyof and indexed access work entirely on types.

keyof: the names of the properties

The keyof operator takes an object type and produces the union of its property names as string literal types. For an interface with three properties, keyof gives a union of three strings, and only those three are accepted:

interface Reading {
  meterId: string;
  kWh: number;
  takenAt: string;
}

type ReadingKey = keyof Reading;     // "meterId" | "kWh" | "takenAt"

function show(reading: Reading, keys: ReadingKey[]): string {
  return keys.map((k) => `${k}=${reading[k]}`).join(", ");
}

const r: Reading = { meterId: "E-1001", kWh: 4.2, takenAt: "2026-10-05T06:00Z" };
console.log(show(r, ["meterId", "kWh"]));
meterId=E-1001, kWh=4.2

Because the union is computed from the interface, renaming or adding a property updates every keyof Reading automatically. A key that does not exist is an error, with a suggestion when it is close:

interface Reading {
  meterId: string;
  kWh: number;
}

const column: keyof Reading = "kwh";
example.ts(6,7): error TS2820: Type '"kwh"' is not assignable to type 'keyof Reading'. Did you mean '"kWh"'?

The most common use is in generic functions: <T, K extends keyof T> accepts only names that exist on the object passed in, as shown in Generics. For a type with an index signature, keyof is as wide as the signature: keyof Record<string, number> is string, and keyof { [key: string]: number } is string | number, because JavaScript turns number keys into strings anyway.

typeof in a type position

JavaScript has a typeof operator that runs while the program runs and returns a string such as "object" or "number". TypeScript reuses the keyword with a different meaning in places where a type is expected. The typeof type operator takes the name of a variable or property and gives the type that TypeScript has for it, so you can name the type of a value without writing it out.

const config = {
  region: "NL",
  vatRate: 0.21,
  tariffs: { day: 0.32, night: 0.25 },
};

type Config = typeof config;            // type position: a TypeScript type
type Tariffs = typeof config.tariffs;   // { day: number; night: number }

function withVat(price: number, c: Config): number {
  return price * (1 + c.vatRate);
}

const cheaper: Tariffs = { day: 0.3, night: 0.22 };
console.log(withVat(cheaper.night, config).toFixed(4));
console.log(typeof config, typeof config.region);   // value position: JavaScript
0.2662
object string

The two typeofs never mix: the one after type Config = is erased with the rest of the types, the one in console.log is ordinary JavaScript and remains in the output.

Type queries

An expression such as typeof config.tariffs in a type position is called a type query: it asks the compiler for the type of a value, given by a variable name or a property path starting at one. A type query cannot contain arbitrary code: no calls, no arithmetic. To get the type a function returns, query the function and use the utility type ReturnType:

function makeTicket(id: string, homes: number) {
  return { id, homes, status: "open" as const, priority: homes > 100 ? 1 : 3 };
}

type Ticket = ReturnType<typeof makeTicket>;

const t: Ticket = makeTicket("INC-10234", 120);
console.log(t);
const wrong: Ticket = { id: "INC-10235", homes: 40, status: "closed", priority: 3 };
example.ts(9,53): error TS2322: Type '"closed"' is not assignable to type '"open"'.

This style suits values that are defined in one place, such as configuration objects and factory functions: the value is the single source of truth, and the type follows it. For data that crosses module or API boundaries, an explicitly written interface is usually clearer, because it documents the contract instead of following whatever the code happens to return.

Mixing up values and types

The two worlds produce two mirror-image errors. Using typeof on something that is only a type:

interface Reading {
  meterId: string;
  kWh: number;
}

type R = typeof Reading;
example.ts(6,17): error TS2693: 'Reading' only refers to a type, but is being used as a value here.

Reading already is a type; write type R = Reading. And using a value where a type is expected, which the compiler helpfully corrects:

const config = { region: "NL", vatRate: 0.21 };

type ConfigKey = keyof config;
example.ts(3,24): error TS2749: 'config' refers to a value, but is being used as a type here. Did you mean 'typeof config'?

Indexed access types

An indexed access type, T["name"], is the type of the property name of type T, written with the same square brackets you use to read a property in JavaScript. It is also called a lookup type. The index can be a single name, a union of names (giving a union of property types), or keyof T (giving the type of any property). Lookups can be chained to reach nested properties.

interface Customer {
  id: number;
  name: string;
  address: { street: string; city: string; postcode: string };
  tariff: "day-night" | "single";
}

type CustomerId = Customer["id"];                    // number
type City = Customer["address"]["city"];             // string
type IdOrName = Customer["id" | "name"];             // number | string
type AnyField = Customer[keyof Customer];            // all four property types

const id: CustomerId = 4471;
const city: City = "Utrecht";
const tariff: Customer["tariff"] = "day-night";
console.log(id, city, tariff);
4471 Utrecht day-night

The point is that Customer["tariff"] stays correct when someone adds a third tariff to the interface; a hand-copied "day-night" | "single" elsewhere would not. Only real property names are allowed as the index:

interface Customer {
  id: number;
  name: string;
}

type Email = Customer["email"];
example.ts(6,23): error TS2339: Property 'email' does not exist on type 'Customer'.

Lookups into arrays and tuples

Arrays are indexed by numbers, so T[number] is the type of an element of the array type T. A tuple type can also be indexed by a specific position. Combined with an as const array, [number] turns a list of allowed values into a union type, so the list and the type always match:

const tariffCodes = ["DAY", "NIGHT", "PEAK"] as const;
type TariffCode = (typeof tariffCodes)[number];     // "DAY" | "NIGHT" | "PEAK"

function isTariffCode(value: string): value is TariffCode {
  return (tariffCodes as readonly string[]).includes(value);
}

for (const input of ["NIGHT", "night", "PEAK"]) {
  console.log(input, isTariffCode(input));
}

type Reading = [meterId: string, kWh: number];     // a labeled tuple
type Usage = Reading[1];                            // number
type Readings = { meterId: string; kWh: number }[];
type OneReading = Readings[number];                 // the element type

const usage: Usage = 4.2;
const one: OneReading = { meterId: "E-1001", kWh: usage };
console.log(one);
NIGHT true
night false
PEAK true
{ meterId: 'E-1001', kWh: 4.2 }

The array of codes exists at run time, so it can check input (the isTariffCode function is a type guard); the union type exists at compile time, so it can check code. Both come from one line. The as readonly string[] widens the tuple type for includes, which would otherwise only accept the three literals.

The parentheses in (typeof tariffCodes)[number] are optional, because typeof binds more tightly than the brackets, but they make the order easy to read.

Combining the operators

The operators are small; their power comes from combining them. The pattern from an as const object instead of an enum reads, from the inside out:

  1. typeof TicketStatus: the type of the object, { readonly Open: "open"; readonly Resolved: "resolved" };
  2. keyof typeof TicketStatus: its keys, "Open" | "Resolved";
  3. (typeof TicketStatus)[keyof typeof TicketStatus]: the types of the properties under those keys, "open" | "resolved".

The same operators guard lookups in plain code. Typing a parameter as keyof typeof tariffs means a caller cannot pass an arbitrary string; input from outside has to be checked first, for example with a type guard like isTariffCode above:

const tariffs = { day: 0.32, night: 0.25 };
type Code = keyof typeof tariffs;            // "day" | "night"

function price(code: Code): number {
  return tariffs[code];
}

const input: string = "day";
console.log(price(input));
example.ts(9,19): error TS2345: Argument of type 'string' is not assignable to parameter of type '"day" | "night"'.

These operators are also the raw material for the utility types (Pick is built from keyof and indexed access) and for mapped types, which loop over keyof T to transform every property.