Type Operators: keyof, typeof and Indexed Access
Building types from other types and values: keyof, the typeof type operator, indexed access types and lookups into arrays and objects.
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.
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:
-
typeof TicketStatus: the type of the object,{ readonly Open: "open"; readonly Resolved: "resolved" }; -
keyof typeof TicketStatus: its keys,"Open" | "Resolved"; -
(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.