Type space vs value space
Every TypeScript identifier lives in value space, type space, or both; `typeof` bridges value-to-type and `keyof` stays type-to-type, and the classic "only refers to a type / a value" errors come from crossing the worlds.
You write function logShape(t: Shape) {} and the editor is happy. You write const s = Shape and the editor underlines Shape in red: “‘Shape’ only refers to a type, but is being used as a value here.” Same word, same file, two completely different verdicts. The reason is the single most clarifying idea in all of TypeScript: there are two parallel namespaces — a value space that exists at runtime and a type space that is erased before it — and most beginner confusion is really one identifier being summoned into the wrong world.
Once you see the two-namespace model, you can decode any “only refers to” error in seconds and navigate typeof, keyof, and class duality with confidence.
Two parallel namespaces
Why does TypeScript need two separate namespaces at all? Because types are erased — the runtime world and the compile-time world are physically separate, and a name can live in one, the other, or both. If you do not know which world a name belongs to, errors like “only refers to a type” will seem arbitrary rather than mechanical.
TypeScript maintains two separate symbol tables. The value space holds things that exist at runtime — variables, functions, the constructor side of a class. The type space holds things that are erased before runtime — type aliases, interfaces, the instance shape of a class. The same name can be registered in one, the other, or both, and position decides which table the compiler consults.
const port = 3000; // `port` is in VALUE space only
type Port = number; // `Port` is in TYPE space only
const x: Port = port; // type position uses Type table, value position uses value table — fine
const y = Port;
// Error: 'Port' only refers to a type, but is being used as a value here.A type annotation position (after :, in extends, inside <...>) is read against the type table. An expression position (the right-hand side of =, a function argument, after return) is read against the value table. Port exists only in the type table, so summoning it in an expression position fails — and the error tells you exactly which world it lives in.
Who lives where
The crucial detail for everyday code: declarations differ in which namespaces they populate.
| Declaration | Value space? | Type space? | Survives to runtime? |
|---|---|---|---|
const x = … / let / function | Yes | No | Yes |
type T = … / interface I | No | Yes | No (erased) |
class C | Yes (constructor) | Yes (instance type) | Yes |
enum E | Yes (object) | Yes (member type) | Yes |
namespace N | Yes | Yes | Yes |
A class is the one that trips people up most: its name means two different things depending on position. In value position it is the constructor function; in type position it is the instance shape.
class Logger {
level = "info";
}
const a = new Logger(); // value position: `Logger` is the constructor
// ^? const a: Logger
function use(l: Logger) {} // type position: `Logger` is the instance type
const ctor: typeof Logger = Logger; // the constructor's TYPE (see next section)The bridges: typeof (value→type) and keyof (type→type)
You frequently have a value and want to talk about its type without writing the type out by hand. The typeof type operator is the bridge from value space into type space.
const config = { host: "localhost", port: 5432, tls: false };
type Config = typeof config;
// ^? type Config = { host: string; port: number; tls: boolean }
function connect(c: typeof config) {} // reuse the inferred shape, no duplicationNote: this is the type-level typeof, used in a type position — it is unrelated to JavaScript’s runtime typeof x operator that returns the string "object". Same keyword, two worlds.
keyof is a different bridge — it stays entirely inside type space, mapping an object type to the union of its keys.
type Config = typeof config;
type ConfigKey = keyof Config;
// ^? type ConfigKey = "host" | "port" | "tls"Compose them and you get the workhorse pattern for typed lookups:
function get<K extends keyof typeof config>(key: K): (typeof config)[K] {
return config[key];
}
const p = get("port");
// ^? number
const h = get("host");
// ^? string
get("nope");
// Error: Argument of type '"nope"' is not assignable to parameter of type
// 'keyof typeof config'. ("host" | "port" | "tls")▸Why this works
Why does typeof config produce a concrete object type rather than just object? Because the type-level typeof reads the static type the checker already inferred for that binding — it does not run anything. For a const whose initializer is an object literal, the checker inferred { host: string; port: number; tls: boolean }, so typeof config is exactly that. If you had written let config or annotated it as Record<string, unknown>, typeof config would yield that wider type instead. The operator is a pointer into the inference result, not a fresh analysis.
The two classic errors, decoded
Both halves of the world-crossing mistake have their own exact diagnostic, and reading the wording tells you which direction you crossed.
interface User { id: string }
const a: User = { id: "1" }; // fine: User in type position
const b = User;
// Error: 'User' only refers to a type, but is being used as a value here.
// (you used a type-only name where a value is required)
const handler = () => {};
type T = handler;
// Error: 'handler' refers to a value, but is being used as a type here.
// Did you mean 'typeof handler'?
// (you used a value-only name where a type is required — bridge it with typeof)The fix is always the same logic: figure out which space the name lives in, and either move to the matching position or use a bridge (typeof) to cross deliberately. And because interface/type are type-only, they vanish at runtime — you cannot instanceof User an interface — whereas a class survives, which is exactly why classes are the tool when you need a runtime-checkable shape. When you encounter either diagnostic in a real codebase, read the wording first: it tells you the direction of the crossing and the bridge you need.
`const settings = { dark: true, lang: 'en' }`. You want a type that is the union `'dark' | 'lang'`. Which expression produces it?
A class name is like a word that means two things by context: in an expression it is the runtime constructor, and in an annotation it is the instance ___ — the shape an instance has.
- 01Which declarations populate the value namespace, the type namespace, or both, and why does it matter at runtime?
- 02What is the difference between type-level `typeof` and `keyof`, and how do they compose?
- 03Decode the two errors: 'X only refers to a type, but is being used as a value here' vs 'X refers to a value, but is being used as a type here'.
The value/type-space split is the lens that makes the rest of the track legible: position chooses a namespace, typeof and keyof are the bridges between them, and the “only refers to” errors are simply a name in the wrong world. You also saw that type-only declarations evaporate at runtime while classes and enums persist — a direct application of the erasure idea from the previous lesson. Now when you see one of those error messages in the editor, you will not guess — you will read the wording, identify which table came up empty, and reach for the right fix or bridge immediately.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.