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 T is not expanded to a fixed union because T is not known yet; it remains a stand-in for "the keys of whatever T turns 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 is never. An unknown value 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

  • keyof is live standalone and in the mapped-key position. A standalone keyof T expression — type K = keyof User on 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