keyof

keyof is the Index Type Query. From the docs:

An indexed type query keyof T yields the type of permitted property names for T. A keyof T type is considered a subtype of string.

—TypeScript docs

The keyof operator takes an object type and produces a string or numeric literal union of its keys.

keyof D2 example

type D2 = {
  kind: "2D";
  x: number;
  y: number;
}

const h: keyof D2 = "‽";

What types and values could be assigned to h?

keyof D2 produces a string union of the keys in D2 in this case because the type D2 defines an object with three properties, and object properties are strings. So, h requires strings. But not any string in the set of all possible strings. It has to be one of "kind" | "x" | "y" because “keyof produces a string union of an object’s keys.” Therefore, those are the only thee possible values we can assign to h.

Intellisense works as expected:

infer 2d intellisense

infer 2d intellisense

keyof D3 example index signature

Suppose this type is in context:

type D3 = {
  kind: "3D"; 
  x: number;
  y: number;
  z: number;
};

Consider:

declare const shape3d: D3;

let k1 = "x";

//
//      k1 indexing errors 😭
//      ---------------------
//                  \
//                   \
//                    v
const val1 = shape3d[k1];
//
// ERROR:
//
// Element implicitly has an 'any' type because expression of type
// 'string' can't be used to index type 'D3'.
//
// No index signature with a parameter of type 'string' was
// found on type 'D3'.
//

k1 is assigned "x", but it could have been changed since its assignment and its use. After all, it is a string declared with let (var would have produced the same outcome), not const.

k1 is of type string, not the union of literal strings "kind" | "x" | "y" | "z", and TypeScript knows those are the only four possible keys in shape3d.

It works though if we use const context when declaring the variable:

declare const shape3d: D3;

var k2 = "x" as const;
let k3 = "x" as const;
const k4 = "x";

const val2 = shape3d[k2];
const val3 = shape3d[k3];
const val4 = shape3d[k4];

All of the above work as expected, correctly indexing the property x of the object.

keyof with getProp() function

Here’s function that takes an prop name and an object, and we return the value of that prop. We use typeof to help us limit the name of possible keys based on the object passed.

type Jedi = {
  name: string;
  skills: string[];
  points: number;
};

function getProp<
  ObjType,
  Prop extends keyof ObjType,
>(propName: Prop, obj: ObjType): ObjType[Prop] {
  return obj[propName];
}

declare const luke: Jedi;

getProp("point", luke);
//
// ERROR:
//
// Argument of type '"point"' is not assignable to parameter
// of type 'keyof Jedi'.
////

getProp<Jedi, keyof Jedi>("points", luke);
//
// OK because ‘points’ is a key in the type ‘Jedi’.
////

Note how we use Prop extends keyof ObjType as the second generic type parameter in the definition of getProp() and then the parameter propName annotated with that type Prop. It causes the type checker to only allow property names that actually exist in the type ObjType, which in our example is bound to the type Jedi.

keyof in type predicates

In this next example, we have a function that displays a prop from an object, but its parameter types are very permissive:

function displayObjProp(
  prop: string | number | symbol,
  obj: Record<string, unknown>,
): void;

Note that prop is string | number | symbol. Those are the ways we can index the keys of an object in JavaScript, so, TypeScript has to consider them all when indexing object types.

And then we turn our attention to the implementation of displayObjProp() in combination with getProp():

function getProp<
  Obj,
  Prop extends keyof Obj,
>(
  propName: Prop,
  obj: Obj,
): Obj[Prop] {
  return obj[propName];
}

function displayObjProp(
  prop: string | number | symbol,
  obj: Record<string, unknown>,
): void {
  log(getProp(prop, obj));
  //           ^
  //          /
  //         /
  //      ERROR 😭
  //
  // Argument of type 'string | number | symbol' is not assignable
  // to parameter of type 'string'.
  // Type 'number' is not assignable to type 'string'.
  //
}

getProp() requires more specific types, but the type of prop in displayObjProp() is too generic.

But we can create a type predicate to make sure the prop really exists in the object.

/**
 * Type predicate to check whether `key` exists in `obj`.
 */
function isKey<Obj>(
  key: string | symbol | number,
  obj: Obj,
): key is keyof Obj {
  return key in obj;
}

Note the use of keyof in the predicate return type!

We can make use of this type predicate (a.k.a type guard) inside displayObjProp() to make sure we only call getProp() if we know the object is sure to contain the given key:

const log: Console["log"] = console.log.bind(console);

type Jedi = {
  name: string;
  skills: string[];
  points: number;
};

/**
 * Type predicate to check whether `key` exists in `obj`.
 */
function isKey<Obj>(
  key: string | symbol | number,
  obj: Obj,
): key is keyof Obj {
  return key in obj;
}

/**
 * Returns the value of the property `propName` from `obj`.
 */
function getProp<
  Obj,
  Prop extends keyof Obj,
>(
  propName: Prop,
  obj: Obj,
): Obj[Prop] {
  return obj[propName];
}

/**
 * Displays the value of a property.
 * 
 * **NOTE**: This is a dummy, convoluted and simple example so
 * we can focus on the types.
 */
function displayObjProp(
  prop: string | number | symbol,
  obj: Record<string, unknown>,
): void {
  if (!isKey(prop, obj)) return;

  log(getProp(prop, obj));
}

const luke: Jedi = {
  name: "Luke Skywalker",
  skills: ["The Force", "Lightsaber"],
  points: 97,
};

displayObjProp("z", { x: 1, y: 2 });
//
// Prints nothing.
////

displayObjProp("name", luke);
//
// → "Luke Skywalker".
////

TS Playground isKey() type predicate.

keyof with mapped types

keyof is also useful to map over types and iterate over its keys.

Partial type utility

type Jedi = {
  name: string;
  skills: string[];
  points: number;
};

type MyPartial<Obj> = {
  [Key in keyof Obj]?: Obj[Key];
  //                ^
  //               /
  //              /
  // Note the use of ‘?’ to cause each mapped property
  // to be made optional.
  //
};

type JediOptionalProps = MyPartial<Jedi>;

const yoda: Jedi = {
  name: "Yoda",
  skills: ["The Force", "Teaching", "Lightsaber"],
  points: 100,
};

const luke: JediOptionalProps = {
  name: "Luke Skywalker",
  //
  // Type is valid, even if we don't provide skills and points.
  //
};

//
// Even empty object satisfies the type.
//
const other: JediOptionalProps = {}

Try commenting out yoda properties to see how it really requires all three properties, while with luke has all of them optional.

Required type utility

In this case we start with a type where each property is optional, and create a type where all of them become required:

//
// Note all properties are optional.
//
type Jedi = {
  name?: string;
  skills?: string[];
  points?: number;
};

/**
 * A type utility to make all properties in `Obj` required.
 */
type MyRequired<Obj> = {
  [Key in keyof Obj]-?:Obj[Key];
  //                ^
  //               /
  //              /
  // Note we do have a “-?” between “]” and “:”.
  //
};

type JediRequiredProps = Required<Jedi>;

const yoda: JediRequiredProps = {
  name: "Yoda",
  // skills: ["The Force", "Teaching", "Lightsaber"],
  points: 100,
};
//
// Property 'skills' is missing in type '{ name: string; points: number; }'
// but required in type 'Required<Jedi>'
///

Uncomment skills above and see the type checker is then appeased 🤣.

Strictly speaking, in my tests, it seems we don’t actually need -? to remove the optional attribute from a property; just not using ? altogether seems to suffices. Still, being explicit makes it evident about what is going on.

References