Search code examples
typescriptmetaprogrammingprivate

TypeScript metaprogramming: inferring private type?


Is it possible to infer the type of a private member in a generic way?

Having this class:

class Dummy {
    private num: number; // I want to get the type of this: number
    str: string; // control group
}

I can get the type of the num field manually:

type DummyNumTypeWorks = Dummy['num']; // number -- yey, I'm happy. 

But I want to do this with generics. I have the following examples that DON'T WORK. And I understand why: Dummy does not implement { num: number }, it does not have a public num member. So I'm searching for alternatives.

// Inferring from Dummy
type DummyMemberType<TMember extends string> = Dummy extends { [key in TMember]: infer R } ? R : never;
type DummyStrMember = DummyMemberType<'str'>; // string
type DummyNumMember = DummyMemberType<'num'>; // never :(

// Inferring from the member name
type DummyMemberType2<TMember extends string> = TMember extends keyof Dummy ? Dummy[TMember] : never;
type DummyStrMember2 = DummyMemberType2<'str'>; // string
type DummyNumMember2 = DummyMemberType2<'num'>; // never :(

I understand why these DON'T work.

My question: while I can infer the type of num manually, is there a way to do this in a generic way (with more metaprogramming)?

Background: I want to create fancy decorators that give guarantees on some other members besides the one being decorated. And some of these members might be private.


Solution

  • Private members are not enumerable in keyof.

    But, as you noted, if you know the name you can dive and get it anyway.

    Given that we can get closer with:

    type DummyMemberType<TMember extends string> = Dummy[TMember];
    // Type 'TMember' cannot be used to index type 'Dummy'.(2536)
    
    type DummyStrMember = DummyMemberType<'str'>; // string
    type DummyNumMember = DummyMemberType<'num'>; // number
    type DummyBadMember = DummyMemberType<'bad'>; // unknown
    

    There's still a type error inside the type, but DummyMemberType now works as it should when used. However, if you pass in a bad key, you get unknown, which isn't ideal.


    So we need to make Dummy indexable by any string to allow us to examine its non enumerable keys. If we intersect it with an index signature we can tell typescript that it's safe to check any string property.

    type DummyMemberType<TMember extends string> =
      (Dummy & { [key: string]: never })[TMember];
    

    Which you could abstract to a generic type alias like so:

    type UniversallyIndexable<T> = T & { [key: string]: never }
    type DummyMemberType<TMember extends string> =
      UniversallyIndexable<Dummy>[TMember];
    

    Now it works like you expect:

    type DummyStrMember = DummyMemberType<'str'>; // string
    type DummyNumMember = DummyMemberType<'num'>; // number
    type DummyBadMember = DummyMemberType<'bad'>; // never
    

    Playground


    Ideally, DummyMemberType<'bad'> would be a type error, but without being able to constrain the key to keyof Dummy, I don't see how that's possible. Returning never may be as good as it gets.