Lesson overview · Free interview practice

Reflection

Explore advanced meta-programming concepts.

Topics in the full lesson
  • Introduction to Reflection and Metaprogramming
  • The Reflect API
  • Using Reflect Methods
  • Dynamic Property Access
  • Metaprogramming with Proxies
  • Best Practices
  • Exercises

Practice Problems

Work through a question before revealing its explanation. These questions and answers are free; Premium adds the full lesson walkthrough, examples and implementation detail.

Completion marks record your own progress, not an automatically checked result. The task type does not determine whether it is optional.

TheoryEasy

The Immutability Ladder: `preventExtensions` / `seal` / `freeze`

Question

JavaScript has three increasingly strict immutability operations on an object. What does each do, and what are the corresponding is* queries?

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

TheoryMedium

Property Descriptors: Data vs Accessor

Question

Every property in JavaScript has a descriptor. What are the two kinds, and how do you read and define them?

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

TheoryMedium

`Reflect` API for Introspection

Question

How does the Reflect API differ from the Object.* methods and the matching operators (in, delete)? When do you prefer Reflect?

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

TheoryMedium

Object Introspection: `keys` / `values` / `entries` / Descriptors / `Reflect.ownKeys`

Question

JavaScript has a small constellation of methods for introspecting an object's properties at runtime. What does each one cover, and when do you reach for which?

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

CodingHard

Class Introspection and Prototype Chain Walking

Implement and check your solution in the stated environment before revealing the answer.

Question

Implement describe(cls) to collect class member metadata across the prototype and constructor chains. Include the actual object that declares each member without invoking getters or constructing an instance.

Input and traversal contract

  • Use ordinary user-defined classes or constructor functions with an own, object-valued data property named prototype. Their inheritance chains are ordinary and stable while inspected. Proxies, built-in subclasses, and manually rewired chains are outside this exercise.
  • Throw TypeError when the input is not a function or lacks that object-valued own data prototype. This is a structural guard, not proof that a value came from class syntax.
  • First walk from cls.prototype toward its ancestors. Stop before Object.prototype or at null.
  • Then walk from cls toward its constructor ancestors. Stop before Function.prototype or at null.
  • At each level, use Reflect.ownKeys order. Include non-enumerable keys and preserve symbol identity.
  • Keep every declaration, including overridden names at different levels. Do not collapse two symbols that happen to have the same description.
  • On the prototype side, skip only the string key 'constructor'. On the static side, skip only 'name', 'length', and 'prototype', even if one was deliberately redefined. Other string and symbol keys remain eligible.
  • Include methods, accessors, and ordinary data properties. Do not inspect private fields or create an instance to discover instance fields.

Output contract

Return an array of rows with exactly these fields:

FieldMeaning
keyOriginal string or symbol key
placement'prototype' or 'static'
kind'method', 'accessor', or 'data'
ownerThe declaring object itself, such as Base.prototype or Base
depthZero at the starting object of that walk, then one per ancestor
descriptorThe full own property descriptor, including flags and function/value references

A data descriptor with a function value is a method. Any accessor descriptor is an accessor, even if both its getter and setter are undefined. Other data descriptors are data.

Do not read a member through owner[key]. Metadata must not execute a getter, setter, constructor, or method. The output order is all prototype rows from nearest to farthest, followed by all static rows from nearest to farthest.

Review the host's descriptor and hierarchy bridge before this advanced extension. You need property descriptors, non-enumerable properties, symbol identity, and prototype traversal.

Local setup

Use a new local folder with Node.js 24.21.0. Save starter.mjs and checks.mjs.

node --version
cp starter.mjs solution.mjs
node checks.mjs

The starter intentionally fails. Implement describe in solution.mjs, then rerun the checks. There are no packages, browser APIs, or network calls.

For TypeScript, save starter.mts and this tsconfig.json. Use TypeScript 5.9.3 with strict checking.

{
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "lib": ["ES2022"],
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "types": [],
    "noEmitOnError": true,
    "outDir": "build"
  },
  "files": ["solution.mts"]
}
tsc --version
cp starter.mts solution.mts
tsc --project tsconfig.json
node checks.mjs ./build/solution.mjs

The .mts files compile to .mjs without a package manifest. Consult the course's environment setup guidance if a tool is unavailable.

Starters

JavaScript starter: starter.mjs

export function describe(cls) {
  throw new Error('Implement describe')
}

TypeScript starter: starter.mts

type MemberRow = {
  key: string | symbol
  placement: 'prototype' | 'static'
  kind: 'method' | 'accessor' | 'data'
  owner: object
  depth: number
  descriptor: PropertyDescriptor
}

export function describe(cls: unknown): MemberRow[] {
  throw new Error('Implement describe')
}

Visible checks

Save these checks as checks.mjs.

import assert from 'node:assert/strict'
import test from 'node:test'

const { describe } = await import(process.argv[2] ?? './solution.mjs')

function fixture() {
  const first = Symbol('same')
  const second = Symbol('same')
  let effects = 0
  class Base {
    ownOnly = 'instance field'
    #hidden = 1
    constructor() { throw new Error('Do not construct') }
    shared() {}
    [first]() {}
    get size() { effects += 1; return this.#hidden }
    set size(value) { effects += 1 }
    static from() {}
    static get status() { effects += 1; throw new Error('Do not read') }
    static category = 'base'
  }
  class Child extends Base {
    shared() {}
    [second]() {}
    static from() {}
  }
  Object.defineProperty(Base.prototype, 'label', {
    value: 'prototype data', enumerable: false, writable: false, configurable: true,
  })
  Object.defineProperty(Base.prototype, 'emptyAccessor', {
    get: undefined, set: undefined, configurable: true,
  })
  return { Base, Child, first, second, effects: () => effects }
}

test('normal: metadata includes exact declaring owners, depths, and descriptors', () => {
  const { Base, Child, effects } = fixture()
  const rows = describe(Child)
  const declarations = rows.filter(row => row.key === 'shared')
  assert.equal(declarations.length, 2)
  assert.equal(declarations[0].owner, Child.prototype)
  assert.equal(declarations[0].depth, 0)
  assert.equal(declarations[1].owner, Base.prototype)
  assert.equal(declarations[1].depth, 1)
  for (const row of rows) {
    assert.deepEqual(Object.keys(row).sort(),
      ['depth', 'descriptor', 'key', 'kind', 'owner', 'placement'])
    assert.deepEqual(row.descriptor, Object.getOwnPropertyDescriptor(row.owner, row.key))
  }
  assert.equal(declarations[0].kind, 'method')
  assert.equal(declarations[0].placement, 'prototype')
  assert.equal(declarations[0].descriptor.enumerable, false)
  const from = rows.filter(row => row.key === 'from')
  assert.deepEqual(from.map(row => [row.owner, row.depth, row.placement]),
    [[Child, 0, 'static'], [Base, 1, 'static']])
  assert.equal(effects(), 0)
})

test('edge: symbols with equal descriptions stay distinct and accessors never run', () => {
  const { Base, Child, first, second, effects } = fixture()
  const rows = describe(Child)
  assert.equal(rows.find(row => row.key === first).owner, Base.prototype)
  assert.equal(rows.find(row => row.key === second).owner, Child.prototype)
  for (const key of ['size', 'status', 'emptyAccessor']) {
    assert.equal(rows.find(row => row.key === key).kind, 'accessor')
  }
  const size = rows.find(row => row.key === 'size')
  assert.equal(size.descriptor.get, Object.getOwnPropertyDescriptor(Base.prototype, 'size').get)
  assert.equal(size.descriptor.set, Object.getOwnPropertyDescriptor(Base.prototype, 'size').set)
  const label = rows.find(row => row.key === 'label')
  assert.equal(label.kind, 'data')
  assert.equal(label.descriptor.value, 'prototype data')
  assert.equal(label.descriptor.writable, false)
  assert.equal(rows.find(row => row.key === 'category').kind, 'data')
  assert.equal(effects(), 0)
})

test('edge: row order follows both walks and stops before built-in roots', () => {
  const { Base, Child } = fixture()
  const rows = describe(Child)
  const expected = [
    ...Reflect.ownKeys(Child.prototype).filter(key => key !== 'constructor')
      .map(key => [Child.prototype, key]),
    ...Reflect.ownKeys(Base.prototype).filter(key => key !== 'constructor')
      .map(key => [Base.prototype, key]),
    ...Reflect.ownKeys(Child).filter(key => !['name', 'length', 'prototype'].includes(key))
      .map(key => [Child, key]),
    ...Reflect.ownKeys(Base).filter(key => !['name', 'length', 'prototype'].includes(key))
      .map(key => [Base, key]),
  ]
  assert.deepEqual(rows.map(row => [row.owner, row.key]), expected)
  assert.equal(rows.some(row => row.owner === Object.prototype), false)
  assert.equal(rows.some(row => row.owner === Function.prototype), false)
  assert.equal(rows.some(row => row.key === 'ownOnly' || row.key === '#hidden'), false)
})

test('edge: exclusions depend on placement, not a global reserved-name set', () => {
  class Named {
    name() {}
    length() {}
    prototype() {}
    static constructor() {}
  }
  const rows = describe(Named)
  assert.deepEqual(rows.map(row => [row.key, row.placement]), [
    ['name', 'prototype'], ['length', 'prototype'], ['prototype', 'prototype'],
    ['constructor', 'static'],
  ])
})

test('edge: empty classes and a null-root prototype finish without construction', () => {
  class Empty {}
  class NullRoot extends null {
    method() {}
  }
  assert.deepEqual(describe(Empty), [])
  const rows = describe(NullRoot)
  assert.equal(rows.length, 1)
  assert.equal(rows[0].key, 'method')
  assert.equal(rows[0].owner, NullRoot.prototype)
  assert.equal(rows[0].depth, 0)
})

test('error: non-functions and functions without own object prototypes are rejected', () => {
  class Sample {}
  for (const invalid of [null, undefined, 1, 'Sample', {}, () => {}, Sample.bind(null)]) {
    assert.throws(() => describe(invalid), TypeError)
  }
})

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

Explore the full Reflection material

Premium includes the complete lessons and implementation references. Free practice questions remain available without a subscription.

All course tracks & premium content
From basics to advanced masterclasses
Built for JS/TS developers like you
Real-world tips & common pitfalls
Upgrade to Premium