Command Palette

Search for a command to run...

Hectal
PHASE 8Intermediate ~22 min· topic 2 of 5

Topic 8.2

typeof in Types

In one line

Inside a type, typeof value asks the compiler "what type does this value have?" so you can derive types from real values and configs instead of writing them twice.

Think of it like this

You have a finished LEGO castle and want a box that fits it exactly. You could measure every tower and write the sizes down, or you could place the castle on a scale and read the result off. **typeof in a type reads the shape off a real value** instead of making you describe it again.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

typeof (in a type)
A compile-time question: what type does this value have? It is erased when the program runs.
typeof (at runtime)
The JavaScript operator that returns text like "string" or "object" while the program is running.
Type position
A place where the compiler expects a type, like after a colon or after type Name =.
Source of truth
The one place a fact is written, so everything else is derived from it.
as const
A suffix that freezes a value into its exact literal types and makes its properties read-only.
Widening
When TypeScript turns the literal 5 into the broader type number because the variable could change.
ReturnType
A built-in helper that gives the type a function returns.
Constructor type
The type of the class itself (what new is called on), written typeof ClassName.
Instance type
The type of an object made with new, written as the class name.

Step by step

01Two typeofs, two worlds

Both of these lines say typeof origin, and they do different jobs. The first is inside a type: the compiler works out the shape of origin and names it Origin. The second is an ordinary JavaScript expression: it runs and prints object.

two-typeofs.tswhole filets
const origin = { x: 0, y: 0, label: "start" };
type Origin = typeof origin;   // type world: { x: number; y: number; label: string }
console.log(typeof origin);    // value world: prints "object"

02Keep one copy

If you add z: 0 to origin, Origin gains z: number on its own. Every function that takes an Origin now expects a z. There is no second definition to forget.

03Configs and as const

A plain object literal gets widened: env: "production" has the type string, because the property could be changed later. Adding as const freezes everything: env is exactly "production", port is exactly 8080, and the array becomes a read-only tuple.

Now index into it: (typeof strict)["env"] is "production", and (typeof strict.features)[number] is "search" | "cart", a union of the real list members. Topic 8.3 explains the square brackets.

04Names and values of a constant object

A common pattern replaces an enum (Topic 5.3) with a frozen object. keyof typeof Status gives the key names ("Idle" | "Loading" | "Done"). Indexing the object type with those names gives the values ("idle" | "loading" | "done").

status.tswhole filets
const Status = { Idle: "idle", Loading: "loading", Done: "done" } as const;

type StatusName  = keyof typeof Status;           // "Idle" | "Loading" | "Done"
type StatusValue = (typeof Status)[StatusName];   // "idle" | "loading" | "done"
Names and values of a constant objectdiagram
Rendering diagram…

05Functions and classes

typeof makeUser is the type of the function. ReturnType and Parameters read it apart. No function is called while the compiler does this.

For a class, Counter is the instance type and typeof Counter is the class itself. A function that takes the class as an argument and calls new on it needs typeof Counter.

06Only names go after typeof

typeof fetchCount() is rejected as a syntax error, because a call is an expression the compiler won't run. Write ReturnType<typeof fetchCount> instead.

Try it yourself

  1. 1

    Grow the value

    In the first example add z: 0 to origin. Predict the new error at the copy line before you read it, then the wrong line. copy now lacks z (TS2741, line 17), and wrong lacks both z and label (TS2739, line 18). Nothing else was edited: Origin grew by itself.

  2. 2

    Remove as const

    In the fourth example delete as const. Predict the new StatusValue. The answer is string: the exact values are gone, and label("paused") stops being an error. StatusName is unchanged, because key names don't depend on as const.

  3. 3

    Ask for a nested path

    In the second example add type Port = typeof strict.port; and hover it: it is 8080. Then try type Bad = typeof strict.nope;: the error is TS2339: Property 'nope' does not exist on type followed by the whole frozen object type.

Code & diagrams

typeof turns values into types

Count is number but Limit is 5: a let can change, a const can't. The last console.log is the runtime typeof, which prints text. The last line is an error because Origin, read off the value, needs a label.

Console

(nothing was printed)

Expected console output

{ x: 5, y: 0, label: 'start' }
object
A config object: widened versus as const

Without as const the values are widened to string and number. With it, everything is exact and read-only, so Feature is a union of the two real list members and "checkout" is rejected.

Console

(nothing was printed)
Functions and classes: ReturnType, Parameters, InstanceType

makeUser has no declared return type, yet User knows its shape because ReturnType reads it. Parameters comes back as a labelled tuple. Counter is the instance type; typeof Counter is the class, which is what make() needs to call new.

Console

(nothing was printed)

Expected console output

{ name: 'Asha', age: 30, createdAt: '2026-10-12' }
Counter { count: 0 }
keyof typeof: the names and the values of a constant object

Add Failed: "failed" to Status and both types grow by themselves. Note the last line: TypeScript rejects "paused" but the code still runs and prints PAUSED.

Console

(nothing was printed)

Expected console output

LOADING
PAUSED

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Use typeof on a type

At the end of the first example add type Bad = typeof Origin;.

terminal
$ Types panel
── what you'll see ──
index.ts(19,19): error TS2693: 'Origin' only refers to a type, but is being used as a value here.

Break #2

Try to call a function inside typeof

Add type Wrong = typeof makeUser("A", 1); to the third example.

terminal
$ Types panel
── what you'll see ──
index.ts(22,29): error TS1005: ';' expected.

Myth vs fact

Myth

typeof x in a type does the same as typeof x in code.

Fact

They only share a spelling. The one in a type is answered by the compiler and erased; the one in code runs and returns text like "object".

Myth

typeof in a type calls the function you give it.

Fact

Nothing is called or run. typeof fn is the type of the function itself.

Myth

typeof Counter and Counter are the same type for a class.

Fact

Counter is the type of an instance; typeof Counter is the class itself, including new and statics.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    typeof in a type is a type query. The compiler resolves the name in the value namespace, and the result is the declared or inferred type of that symbol. It accepts a dotted path (typeof strict.features, as in the second example) but never a call or any other expression.

  • ▸

    Because the type is read from the value, an unannotated value can't be defined in terms of its own type: const a = { next: null as unknown as typeof a }; fails with TS7022: 'a' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer. Write one annotation (for example type ListNode = { next: ListNode | null } and const a: ListNode = ...) to break the cycle.

  • ▸

    as const objects are the current alternative to enum in code that must be erasable (Topic 5.3): there is no extra runtime object generated beyond your own constant. Under erasableSyntaxOnly (TypeScript 5.8) real enums are not allowed at all.

Remember this

  1. 1

    The problem this solves: you already have a value (a settings object, a function, a class) and need its type in a signature or alias, and you don't want to write that shape out a second time. JavaScript already has a typeof operator that runs in your program and returns text like "string" or "object". TypeScript reuses the same word in a second place: in a type position (after a colon, after type X =, inside angle brackets), typeof value is evaluated by the compiler, not at runtime, and gives the type of that value. The two share a name but live in different worlds: the runtime one gives text, the type one gives a type, and the type one disappears when the program runs.

  2. 2

    The value must really exist as a variable, a function or a class. typeof origin is fine; typeof Point where Point is only a type is an error ('Point' only refers to a type, but is being used as a value here), because types aren't values and have no type of their own to ask about.

  3. 3

    Why use it? When you already have a value, say a settings object, writing a second type Settings = { ... } by hand creates two copies that can drift apart. type Settings = typeof settings makes the value the source of truth. The type is whatever the value says it is, including the let versus const widening rules from Topic 1.3: typeof a let n = 5 is number, but typeof a const m = 5 is 5.

  4. 4

    The best partner is as const (Topic 4.2): it freezes an object into its exact literal values and makes it readonly. Then typeof config has the exact strings and numbers, and keyof typeof config gives the config's key names as a union. The pair keyof typeof Status is so common you'll type it without thinking. (typeof Status)[StatusName] gives all the values. The parentheses are optional (typeof Status[StatusName] means the same thing, checked), but they make the reading clear, and many style guides add them.

  5. 5

    typeof also works on functions, which is how you get the types of a function you didn't write or didn't want to declare twice. ReturnType<typeof makeUser> is the type the function returns, and Parameters<typeof makeUser> is a tuple of its parameter types. Both are built-in helper types you'll see built from scratch in Topic 9.3 and listed in Topic 10.2. No function call is made: typeof makeUser is about the function itself.

  6. 6

    With a class, there are two types: the type of its instances (written Counter) and the type of the class itself, the constructor with its static members (written typeof Counter). If a function receives the class (so it can call new C()), its parameter type is typeof Counter. InstanceType<typeof Counter> turns the class type back into the instance type.

  7. 7

    You can't put an expression into typeof in a type. typeof fetchCount() is a syntax error, because a call is something that runs. Only names and property paths like typeof config.server.port are allowed. To get what a call returns, use ReturnType<typeof fetchCount>.

Explain it without notes

01

What is the difference between typeof x in a type and typeof x in an expression?

02

How do you get the union of a constant object's keys, and the union of its values?

03

How do you get the return type of a function without writing it twice?

04

What is the difference between Counter and typeof Counter for a class?

Practice

01

Given const sizes = { small: 1, medium: 2, large: 3 } as const;, write the types SizeName and SizeValue, and a function price(size: SizeName): SizeValue.

02

Given a function function loadUser() { return { id: 1, name: "Asha", admin: false }; }, declare a variable u whose type is the returned value, without writing the object shape by hand.

Trade-offs

  • ↔

    Deriving types from values keeps one source of truth, but a reader has to follow the chain typeof -> keyof -> index to see the final shape. Hover the alias in your editor, or add a // ^? while learning, and give long chains a name.

  • ↔

    as const makes values exact and read-only. That is what you want for configs and lookup tables, and too strict for data you plan to change, where a normal annotation is better.

Done when you can

  • I can explain the difference between typeof x in a type and in code.

  • I can turn a constant object into a type with typeof.

  • I can write keyof typeof obj and (typeof obj)[keyof typeof obj] and say what each gives.

  • I can use ReturnType<typeof fn> and Parameters<typeof fn>.

  • I can tell Counter from typeof Counter.