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.)
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.
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 membersWhen 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.
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.
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:
{ [K in keyof T]: ... } never iterates over private or protected keys in the first place. keyof already excluded them, so there's nothing to filter out.IsPrivate<T, K>: since private keys never appear in any key query, no conditional type can even name them to test them.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.
The single test Equal<ClassPublicKeys<A>, 'str' | 'getNum'> checks the exact union, no extras, no order sensitivity (unions are unordered sets):
'str': public field, included.'getNum': method, included. Note the constructor is not a key of the instance type either, so it correctly doesn't appear.'num' and 'bool': protected and private, invisible to keyof, correctly excluded.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.