TypeScript typeof
TypeScript has two operators called typeof, and they do completely different things. One you inherited from JavaScript, the other is one of the most useful tools in the type system.
In this article we will cover both, when each one applies, and the patterns that make the type-level typeof worth reaching for every day: extracting types from config objects, building string unions with keyof typeof, and pairing it with ReturnType and Parameters.
Two operators, one name
JavaScript's typeof runs at runtime and returns a string. TypeScript's typeof runs at compile time and returns a type. Which one you get depends entirely on where you write it:
let value = 42
// JavaScript: expression position, evaluates at runtime
console.log(typeof value) // prints "number"
// TypeScript: type position, resolved at compile time
type ValueType = typeof value // numberThe rule is simple. If typeof appears where a value is expected, it is the JavaScript operator. If it appears where a type is expected — after a :, inside a type alias, in a generic argument — it is the TypeScript operator. Same keyword, different worlds.
The compiled output makes the difference obvious. The runtime typeof survives compilation and executes in the browser or in Node. The type-level typeof is erased along with every other type annotation — it produces zero JavaScript and exists purely for the compiler.
Searchers coming from JavaScript usually know the first one. The rest of this article is about the second, because that is where the real payoff sits.
Extracting a type from a value
The core job of type-level typeof is answering one question: "what is the type of this thing I already wrote?" Instead of describing a shape twice — once as a value, once as an interface — you write the value and let TypeScript derive the type:
const config = {
endpoint: 'https://api.example.com',
retries: 3,
verbose: false,
}
type Config = typeof config
// { endpoint: string; retries: number; verbose: boolean }
function applyConfig(overrides: Partial<Config>): Config {
return { ...config, ...overrides }
}
applyConfig({ retries: 5 }) // ✅
// applyConfig({ retries: '5' }) // ❌ string is not assignable to numberThe value is the single source of truth. Add a field to config and Config updates itself — no interface to keep in sync, no drift between the two. This matters most for objects that change often: configuration, theme tokens, route tables, test fixtures.
Reaching into nested values
You are not limited to the top level. The type typeof produces can be indexed like any other, so you can pull out exactly the slice you need:
const theme = {
colors: {
primary: '#0057ff',
surface: '#ffffff',
},
spacing: [0, 4, 8, 16],
}
type Colors = (typeof theme)['colors'] // { primary: string; surface: string }
type ColorName = keyof (typeof theme)['colors'] // 'primary' | 'surface'
type Spacing = (typeof theme)['spacing'][number] // numberA component that only cares about colors can take (typeof theme)['colors'] instead of the whole theme type. The parentheses around typeof theme matter — without them TypeScript parses the expression differently — so keep them whenever you index into a typeof result.
typeof with functions
Functions are where typeof starts combining with the built-in utility types. On its own, typeof someFunction gives you the full function type. Feed it into generics like ReturnType and Parameters and you can pull the pieces apart:
function createUser(name: string, age: number) {
return { id: Math.random(), name, age }
}
type CreateUserFn = typeof createUser
type User = ReturnType<typeof createUser> // { id: number; name: string; age: number }
type CreateUserArgs = Parameters<typeof createUser> // [name: string, age: number]
const mockCreateUser: CreateUserFn = (name, age) => ({ id: 0, name, age })Notice that createUser never declares a return type, and there is no User interface anywhere. The function's inferred return type is the type, and ReturnType<typeof createUser> hands it to you. This is the standard trick for typing mocks, wrappers, and anything that needs to match an existing function's signature exactly.
If you want to understand what ReturnType and Parameters do under the hood, the Get Return Type and Parameters challenges have you implement both from scratch. Parameters<typeof fn> is probably the single most common real-world pairing of typeof with a utility type.
typeof with classes
Classes are the one place where the value/type split regularly trips people up. A class declaration creates two things at once: a type describing its instances, and a value — the constructor — you call with new. The class name used as a type refers to the instance. To talk about the constructor itself, you need typeof:
class ApiClient {
constructor(public baseUrl: string) {}
get(path: string): string {
return this.baseUrl + path
}
}
// instance type: what `new ApiClient(...)` produces
function useClient(client: ApiClient): void {
console.log(client.get('/users'))
}
// constructor type: the class itself, so we can `new` it
function makeClient(Ctor: typeof ApiClient, url: string): ApiClient {
return new Ctor(url)
}
useClient(makeClient(ApiClient, 'https://api.example.com'))Annotate a parameter as ApiClient and you can call its methods but not new it. Annotate it as typeof ApiClient and it is the other way around. Factory functions, dependency injection, and registry patterns all rely on this distinction — the moment you pass a class around as a value, typeof is how you type it.
keyof typeof: string unions from object keys
This combination shows up in almost every serious TypeScript codebase. You have an object, and you want a type that is exactly its keys:
const STATUS_COLORS = {
active: 'green',
pending: 'orange',
banned: 'red',
} as const
type Status = keyof typeof STATUS_COLORS // 'active' | 'pending' | 'banned'
function getColor(status: Status): string {
return STATUS_COLORS[status]
}
getColor('active') // ✅
// getColor('deleted') // ❌ 'deleted' is not assignable to StatusRead it inside out: typeof STATUS_COLORS gives the object's type, keyof gives that type's keys as a union type. Add a new status to the object and every function taking a Status accepts it immediately — remove one and every stale call site turns red.
This pattern is also the most popular alternative to enums. The object exists at runtime for lookups, the union exists at compile time for safety, and there is no extra generated code. From here it is a short step to mapped types, which let you build new object shapes from those same keys.
The same combination works on actual enums, because an enum is also a value with keys. keyof typeof turns its member names into a string union:
enum LogLevel {
Debug = 0,
Info = 1,
Error = 2,
}
type LogLevelName = keyof typeof LogLevel // 'Debug' | 'Info' | 'Error'
function setLevel(levelName: LogLevelName): void {
console.log(LogLevel[levelName]) // safe reverse lookup
}
setLevel('Info') // ✅
// setLevel('Warning') // ❌ 'Warning' is not assignable to LogLevelNameThis is the idiomatic way to accept an enum member by name — from a config file, an environment variable, or an API payload — while keeping the compiler involved. Without keyof typeof you would be validating those strings by hand.
as const + typeof: precise types from arrays
By default TypeScript widens literals: an array of strings becomes string[], and the individual values are forgotten. as const stops the widening, and typeof picks up the precise result:
const roles = ['admin', 'editor', 'viewer'] as const
type Roles = typeof roles // readonly ['admin', 'editor', 'viewer']
type Role = (typeof roles)[number] // 'admin' | 'editor' | 'viewer'
function assignRole(role: Role): void {
console.log(role)
}
assignRole('editor') // ✅
// assignRole('owner') // ❌ 'owner' is not assignable to RoleThe (typeof roles)[number] part indexes the tuple with number, which means "the union of everything at any index". One as const array now drives both your runtime iteration and your compile-time checking. The Tuple to Object challenge builds directly on this pattern — its test cases hand you typeof an as const tuple and ask you to transform it.
The runtime typeof still matters: narrowing
Back to the JavaScript operator for a moment, because TypeScript gives it a second job. Inside a condition, a runtime typeof check narrows the type:
function formatValue(input: string | number): string {
if (typeof input === 'number') {
return input.toFixed(2) // input is number here
}
return input.toUpperCase() // input is string here
}TypeScript understands the check and narrows input in each branch — this is the standard way to work with union types at runtime. No casting, no assertion functions, just a plain JavaScript condition the compiler can follow.
One honest caveat: runtime typeof only distinguishes JavaScript's primitive buckets. Arrays, null, and plain objects all report "object", so for anything structural you need Array.isArray, a discriminant property, or a custom type guard instead.
What typeof cannot do
The type-level typeof takes the name of a value — a variable, a property chain, a function identifier. It does not evaluate expressions:
function getSettings() {
return { theme: 'dark', fontSize: 14 }
}
// type Settings = typeof getSettings() // ❌ syntax error — you cannot call a function here
type Settings = ReturnType<typeof getSettings> // ✅ this is what ReturnType is forIf you catch yourself wanting typeof someCall(), the answer is always ReturnType<typeof someCall>. The same logic applies to awaited values: Awaited<ReturnType<typeof fetchUser>> gets you the resolved type of an async function.
Also keep the direction straight: typeof goes from value to type, never the other way around. Types are erased at compile time, so there is no operator that turns an interface into a runtime object. If you need both, define the value first and derive the type — that is exactly the workflow this article has been showing.
Where to practice
Reading about typeof gets you halfway; the type-level challenges make it stick, because each one hands you a value and forces you to derive something precise from it:
- Parameters – implement
Parameters<T>, the utility you will most often feedtypeofinto - Get Return Type – implement
ReturnType<T>and see how inference extracts whattypeofcaptured - Tuple to Object – work directly with
as const+typeoftuples
The takeaway
typeof in TypeScript is two tools sharing a name. The runtime operator narrows union types in conditions. The type-level operator turns values you already wrote into types you never have to maintain by hand — on its own for objects and functions, with keyof for key unions, with as const for precise literals, and with ReturnType and Parameters for function surgery.
The mindset shift is treating values as the single source of truth and deriving the types from them with typeof, instead of writing every type twice and keeping both copies in sync by hand. Next time you catch yourself writing an interface that mirrors an object one line above it, delete the interface and write typeof instead.
Become a TypeScript Pro
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges