TypeScript keyof: Keys of a Type

November 9, 202611 min read
Requirements:
ObjectsGenerics| Unions

People search for this one as "key of", but the operator is a single word: keyof. It takes a type and gives you back the union of that type's keys. That is the whole idea, and it is the foundation under Partial, Pick, Omit, Record, and most mapped types you will ever write.

In this article we will cover what keyof returns for every kind of type you can hand it, the two cases that surprise people, and the pattern that makes it worth learning: using it as a generic constraint so the compiler knows which property you are reaching for.

What keyof gives you

Point keyof at an object type and you get a union of its key names as string literals:

type UserBasic = {
  id: number
  name: string
  email: string
}
 
type UserKeys = keyof UserBasic
// "id" | "name" | "email"

UserKeys is not string. It is a union of exactly three literal types. Anything outside that set is a compile error:

type UserBasicRef = {
  id: number
  name: string
  email: string
}
 
const goodKey: keyof UserBasicRef = 'email'
 
// const badKey: keyof UserBasicRef = 'phone'
// Error: Type '"phone"' is not assignable to type '"id" | "name" | "email"'

That narrowness is the entire point. A string can be anything. A keyof union can only ever be a key that actually exists on the type, so typos get caught where you write them instead of at three in the morning in production.

The operator works on interfaces too — there is no difference:

interface ProductShape {
  sku: string
  price: number
}
 
type ProductKeys = keyof ProductShape
// "sku" | "price"

Why the result is a union, not a list

A common first assumption is that keyof returns something array-like. It does not. It returns a union type, which means you use it everywhere a union is useful — in extends constraints, in in clauses, as a function parameter type.

type ColorTheme = {
  background: string
  foreground: string
  border: string
}
 
type ThemeSlot = keyof ColorTheme
 
function describeSlot(slot: ThemeSlot): string {
  return `theme slot: ${slot}`
}
 
describeSlot('border') // fine
 
// describeSlot('padding')
// Error: Argument of type '"padding"' is not assignable to parameter of type 'ThemeSlot'

Because it is a union, autocomplete works. Type describeSlot(' in your editor and all three slots appear. That alone pays for the operator.

Unions are worth understanding properly before you lean on keyof heavily — our guide to union types covers how they narrow and how they combine.

keyof with index signatures

Here is the first surprise. When a type has an index signature instead of named properties, keyof cannot name individual keys, so it hands back the index type:

type StringDict = {
  [key: string]: number
}
 
type StringDictKeys = keyof StringDict
// string | number

That | number catches almost everyone. It is not a bug. JavaScript converts numeric keys to strings at runtime, so obj[0] and obj["0"] reach the same property. A string index signature therefore genuinely accepts numbers, and TypeScript reflects that.

A number index signature behaves the way you would expect:

type NumberDict = {
  [key: number]: string
}
 
type NumberDictKeys = keyof NumberDict
// number

The practical takeaway: if you are reaching for keyof to get a precise set of key names, an index signature will not give you one. You want a literal object type, a Record with a union key, or keyof typeof on a real value.

keyof on arrays and tuples

The second surprise. Arrays are objects, and keyof dutifully reports everything on them:

type StringArrayKeys = keyof string[]
// number | "length" | "toString" | "pop" | "push" | "concat" | ... and the rest of Array.prototype

This is almost never what people want. If you are after the element type, index into the array type instead:

type Tags = string[]
type TagItem = Tags[number] // string

Tuples behave the same way, except their numeric keys are literals:

type PairTuple = [name: string, age: number]
 
type PairIndex = Exclude<keyof PairTuple, keyof unknown[]>
// "0" | "1"
 
type PairMember = PairTuple[number] // string | number

Reach for Tuple[number] when you want the members, and only filter keyof when you genuinely need the positions.

keyof on classes, unions, and intersections

Classes work like object types, with one thing to watch: you get the public instance members, methods included, and nothing private.

class InvoiceModel {
  id = 0
  total = 0
  private secret = 'hidden'
 
  render(): string {
    return `invoice ${this.id}`
  }
}
 
type InvoiceKeys = keyof InvoiceModel
// "id" | "total" | "render"

secret is absent because keyof only sees what is accessible from outside. Methods are present because, to the type system, a method is just a property whose type happens to be a function.

Unions and intersections flip in a way that looks backwards until you think it through:

type CatShape = { name: string; purrs: boolean }
type DogShape = { name: string; barks: boolean }
 
type EitherKeys = keyof (CatShape | DogShape)
// "name" — only what both have
 
type BothKeys = keyof (CatShape & DogShape)
// "name" | "purrs" | "barks" — everything from either

A union value could be either shape at runtime, so the only key you can safely access is one that exists on both. An intersection value is guaranteed to have everything from both, so you get the full set. keyof over a union gives you the intersection of keys, and over an intersection gives you the union — that inversion is worth memorising.

One last corner: keyof any resolves to string | number | symbol. You will see it as the constraint on Record and on your own helpers, where it means "anything that can legally be a key".

type AnyKey = keyof any
// string | number | symbol
 
type LookupOf<K extends keyof any, V> = { [P in K]: V }

The payoff: keyof as a generic constraint

This is the pattern that makes the operator worth learning. A function that reads a property off an object should know which properties exist and what type each one returns. K extends keyof T does exactly that:

function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}
 
const accountRecord = {
  id: 7,
  label: 'primary',
  active: true,
}
 
const accountId = getProp(accountRecord, 'id') // number
const accountLabel = getProp(accountRecord, 'label') // string
const accountActive = getProp(accountRecord, 'active') // boolean
 
// getProp(accountRecord, 'owner')
// Error: Argument of type '"owner"' is not assignable to parameter of type '"id" | "label" | "active"'

Look at what the compiler worked out. Each call returns a different type, inferred from the key you passed. No casts, no overloads, no any. T[K] is an indexed access type: "the type of property K on type T".

The same constraint makes setters safe:

function setProp<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
  obj[key] = value
}
 
const settingsRecord = { volume: 50, muted: false }
 
setProp(settingsRecord, 'volume', 80)
 
// setProp(settingsRecord, 'volume', 'loud')
// Error: Argument of type 'string' is not assignable to parameter of type 'number'

Pass the wrong value type for a key and it fails at the call site. Compare that to obj[key] = value with key: string, where everything is permitted and nothing is checked.

Getting value types with T[keyof T]

Flip the pattern around. Instead of indexing with one key, index with the whole union and you get the union of every value type:

type StatusMap = {
  draft: 'DRAFT'
  review: 'IN_REVIEW'
  live: 'PUBLISHED'
}
 
type StatusValue = StatusMap[keyof StatusMap]
// "DRAFT" | "IN_REVIEW" | "PUBLISHED"

This is the standard way to turn a lookup object into a union of its values, and it shows up constantly in code that replaces enums with plain objects.

Filtering keys by their value type

Once you can iterate keys you can also filter them. Remapping a mapped type to never drops the key entirely, which lets you select only the properties whose values match a type you care about:

type FormModel = {
  title: string
  slug: string
  views: number
  published: boolean
}
 
type KeysOfType<T, V> = {
  [K in keyof T]: T[K] extends V ? K : never
}[keyof T]
 
type TextFields = KeysOfType<FormModel, string>
// "title" | "slug"

The inner mapped type turns every key into either itself or never, and the trailing [keyof T] collapses that object into a union of its values. never vanishes from unions automatically, so only the matching keys survive.

This is the trick behind a lot of library types — picking all the event handlers off a props type, or every string column off a database row.

keyof vs Object.keys()

This is the single biggest source of confusion, so it is worth being blunt about it: keyof and Object.keys() are not two ways of doing the same thing. One is a type-level operator that disappears at compile time. The other is a runtime function that returns an array.

type ProfileShape = {
  handle: string
  followers: number
}
 
const profileValue: ProfileShape = { handle: 'philip', followers: 42 }
 
type ProfileKeyUnion = keyof ProfileShape // "handle" | "followers"
 
const profileKeyList = Object.keys(profileValue) // string[] — not ProfileKeyUnion[]

Object.keys() is typed as returning string[], and that is deliberate on TypeScript's part. An object can carry more properties at runtime than its type declares — anything assigned to ProfileShape elsewhere in your program might have extra keys — so narrowing the return to the declared keys would be unsound.

When you know the object has no extras, assert it:

type AccountShape = {
  handle: string
  followers: number
}
 
const accountValue: AccountShape = { handle: 'philip', followers: 42 }
 
const accountKeyList = Object.keys(accountValue) as (keyof AccountShape)[]
 
accountKeyList.forEach((entryKey) => {
  const entryValue = accountValue[entryKey] // string | number
  console.log(entryKey, entryValue)
})

That assertion is a promise you are making to the compiler. Make it only when you control the object. If you want to iterate keys and values together with better typing out of the box, Object.entries is usually the nicer tool.

Where keyof shows up next

Three things build directly on what you just read, and each has its own page here.

keyof typeof is how you apply the operator to a value rather than a type. You cannot write keyof someObject — keyof wants a type — so you convert first:

const routeConfig = {
  home: '/',
  about: '/about',
  pricing: '/pricing',
}
 
type RouteName = keyof typeof routeConfig
// "home" | "about" | "pricing"

That two-operator combo is the most common keyof usage in real codebases. Our guide to the typeof operator covers it in depth, including when to reach for as const.

Mapped types iterate a keyof union to build a new object type. [K in keyof T] is the syntax, and it is how Partial, Readonly, and every custom transform you write are implemented:

type NullableAll<T> = {
  [K in keyof T]: T[K] | null
}
 
type DraftPost = NullableAll<{ title: string; body: string }>
// { title: string | null; body: string | null }

Our guide to mapped types goes through the modifiers and key remapping.

Utility types are mapped types the standard library already wrote for you. Record builds an object from a key union, and Omit uses keyof plus Exclude to drop keys:

type ArticleShape = {
  id: number
  title: string
  draft: boolean
}
 
type PublicArticle = Omit<ArticleShape, 'draft'>
// { id: number; title: string }

Wrapping up

keyof turns a type into the union of its keys, and that union is the hinge for a lot of TypeScript. Remember the two gotchas — index signatures give you string | number, and arrays give you every Array.prototype method — and the rest follows naturally.

The pattern to actually take away is K extends keyof T paired with T[K]. Once that clicks, property access stops being a place where types go to die, and starts being a place where the compiler does your checking for you.

When you want to practise, the optional keys and required keys challenges both lean on keyof and are a good test of whether it has sunk in.

Share this article

Become a TypeScript Pro

Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges

Practice with Challenges

Put your typescript keyof: keys of a type knowledge to the test with these related challenges.

#90Optional Keys
Hard
#89Required Keys
Hard
#5181Mutable Keys
Hard

Related Concepts

Concepts that build on or relate to typescript keyof: keys of a type.

TypeScript typeofMapped TypesUnion TypesTypeScript RecordTypeScript Omit