keyof
keyof T produces the union of a type's known property keys as string literals, which you can iterate, constrain against, or feed to an indexed lookup. It stays symbolic inside a generic and yields never for unknown, since a value you have not checked exposes no keys.
keyof T gives you the union of a type's statically-known keys, each as a literal type. If T has properties id and name, keyof T is "id" | "name".
type User = { id: number; name: string }
// keyof User is "id" | "name"
This is the same operator you know from TypeScript, with one deliberate difference covered below. It turns the shape of a type into a value you can compute over: a set of keys you can iterate, constrain against, or feed to an indexed access.
What it yields
keyof resolves differently depending on how much the checker knows about T:
- Of a concrete object type, it is the union of that object's key literals.
keyof { a: number; b: string }is"a" | "b". - Of an unresolved type parameter, it stays symbolic. Inside a generic,
keyof Tis not expanded to a fixed union becauseTis not known yet; it remains a stand-in for "the keys of whateverTturns out to be." This symbolic form is exactly what makes a mapped type homomorphic — the mapping is written against the source's own keys, so it can preserve the source's per-property modifiers. - Of
unknown, it isnever. Anunknownvalue exposes no keys, so the set of its keys is empty.
type K1 = keyof { a: number; b: string } // "a" | "b"
type K2 = keyof unknown // never
The difference from TypeScript
In TypeScript, keyof any is string | number | symbol — the widest possible key set. CruftScript has no any, and keyof unknown is never instead of that wide union. The practical effect is that a mapping keyed off an unverified value produces nothing rather than exploding into every conceivable key. An empty result is easier to reason about, and harder to misread, than a catch-all one.
Why it works this way
Keys are only meaningful when the checker actually knows the shape they come from. For a concrete object it does, so you get the exact literals. For a type parameter it does not yet, so the operator stays symbolic and waits for instantiation rather than guessing. For unknown — a value you have not checked — there is no shape at all, so there are no keys. Making keyof unknown empty keeps type-level code from quietly computing over a key set that was never real.
Limitations
keyofis live standalone and in the mapped-key position. A standalonekeyof Texpression —type K = keyof Useron its own — parses and resolves to the key union, and that alias can then be used as an index (type V = User[K]) or as the key source of a mapping ([K in keyof T]).
[K in keyof T] // works
type K = keyof User // works: resolves to the key union