#2828Hard

ClassPublicKeys

Extract the public keys of a class into a union. The answer is one line, because keyof never sees private or protected members in the first place.

The whole solution is one line, and the point of the challenge is understanding why one line is enough.

ClassPublicKeys<T> returns all public keys of a class, filtering out anything marked private or protected. It is a good excuse to learn how TypeScript actually models access modifiers: whether private fields are part of the type at all, and whether a mapped type can iterate over them. The answers are more interesting than the solution suggests.

class A {
  public str: string
  protected num: number
  private bool: boolean
  getNum() {
    return Math.random()
  }
}
 
type publicKeys = ClassPublicKeys<A> // 'str' | 'getNum'

(A side note if you paste this snippet into a strict playground: strictPropertyInitialization will complain that the fields are never assigned. The challenge's actual test file initializes them in a constructor, omitted here for brevity. Read the snippet as a declare class.)

Challenge Instructions: ClassPublicKeys

Hard

Implement the generic ClassPublicKeys<T> which returns all public keys of a class.

For example:

class A {
public str: string
protected num: number
private bool: boolean
getNum() {
return Math.random()
}
}
 
type publicKeys = ClassPublicKeys<A> // 'str' | 'getNum'

View on GitHub: https://tsch.js.org/2828

Change the following code to make the test cases pass (no type check errors).

👋 Lifetime-License is leaving on August 10, 2026

Get it now for $29

One-time payment. Lifetime access to all pro challenges.

Loading...

Detailed Explanation

The solution, in its entirety:

[object Object]

Yes, really. The entire challenge dissolves into a single keyof. Why that works is the actual lesson, and it says a lot about how TypeScript models classes.

keyof only sees public members

When you apply keyof to a class instance type, TypeScript deliberately returns only the publicly accessible keys. Private and protected members are visible inside the class (and, for protected, inside subclasses), but from the outside, which is where keyof T conceptually stands, they don't exist as accessible properties:

[object Object]

num (protected) and bool (private) are absent. Method names like getNum are included, because methods are public properties like any other. At the key level there is no distinction between a field and a method.

Why the language works this way

TypeScript's access modifiers are enforced through assignability from the outside. If keyof A included 'bool', you could write A['bool'] or a mapped type touching it from external code, and privacy would leak through every generic utility. Instead, the type-level view of a class matches the runtime contract of its public API. This is also why private members make classes behave nominally in an otherwise structural type system: two classes with identical shapes but separately declared private fields are not assignable to each other, because each private member is tied to its originating declaration.

The trap: trying to filter manually

Most people meet this challenge and reach for heavy machinery: mapped types with key remapping, or conditional filters comparing T against Readonly<T>. The instinct is reasonable but misdirected, and it is worth understanding why the machinery can't work:

In other words, the filtering you're asked to implement has already happened by the time you can inspect the type. The only job left is to surface it.

What the test verifies

The single test Equal<ClassPublicKeys<A>, 'str' | 'getNum'> checks the exact union, no extras, no order sensitivity (unions are unordered sets):

The meta-lesson: before building type machinery, check what the language already guarantees. Some "hard" challenges are hard because the simple answer feels too simple to trust.

This challenge is originally from here.

Share this challenge

Learn the Concepts