0

I'm having trouble figuring out this simple (on the surface) problem. I'm looking to create a type that has the keys of both the parent class, and the current child I'm working on. I have these classes:

class A {
  foo = 1;
  xoo = 'abc';

  get(){
    return {} as ?;
  }
}

class B extends A {
  bar = 2;
  zar = [1, 2, 3];

  init() {
    this.get(). // < -- should autocomplete and show both "foo" and "bar".
  }
}

As you can see, I would like to have this.get() return a type that keys of both the Parent and Child that are of type number. In this case, the autocomplete should show both "foo" and "bar" as options, but not "init", "get", "xoo" or "zar". I've for a long while tried to get this working, but I think it's just beyond my knowledge at this point. Any help in accomplishing this and understanding the solution would be massively helpful!

Craig
  • 53
  • 1
  • 6
  • Does this answer your question? [Get properties of a class](https://stackoverflow.com/questions/40636292/get-properties-of-a-class) – Henry Woody Sep 06 '21 at 17:31
  • 1
    How should your type decide which properties to ignore? Are you literally giving those properties names beginning with `ignoreMe`? – kaya3 Sep 06 '21 at 17:40
  • @kaya3 I'll fix the example. They are to be ignored because they aren't a number. – Craig Sep 06 '21 at 18:05

2 Answers2

2

First of all, you should properly type get method:


type OnlyNumbers<T> = {
  [Prop in keyof T]: T[Prop] extends number ? Prop : never
}[keyof T]

const pickNumbers = <Obj,>(obj: Obj) =>
  (Object.keys(obj) as Array<keyof Obj>)
    .reduce((acc, elem) =>
      typeof obj[elem] === 'number' ? {
        ...acc,
        [elem]: obj[elem]
      } : acc, {} as Pick<Obj, OnlyNumbers<Obj>>)


class A {
  foo = 1;
  xoo = 'abc';

  get = () => pickNumbers(this)
}

Now we can explicitly type our this:

class B extends A {
  bar = 2;
  zar = [1, 2, 3];

  init(this: B & A) {
    this.get().bar // ok
    this.get().foo // ok
    this.get().zar // error
  }
}

// "bar" | "foo"
type Test = keyof {
  [Prop in keyof Pick<B, OnlyNumbers<B>>]: Prop
}

Playground

You can get rid of A&B in init but then you loose autocomplete. From the other hand you will still have autocomplete out of the class scope:

class B extends A {
  bar = 2;
  zar = [1, 2, 3];

  init() {
    this.get()
  }
}

const result = new B().get() // foo | bar
  • This is really great work. I'll have to study it for a while. For the library I'm writing, though, I worry it would be too much to ask the next guy to have to include the this typing if they wanted autocomplete (this: B & A). Is there any way you can imagine this working without specifying the type of this like you did? – Craig Sep 07 '21 at 02:09
  • I did some poking around and tried Pick>. As you probably know, no luck. Is something like this not possible? – Craig Sep 07 '21 at 02:57
  • 90% that it is impossible to do. Seems to be that TS treats `this` as a black box – captain-yossarian from Ukraine Sep 07 '21 at 08:09
0

Try something like this:

class A<T> {
  foo = 1;
  ignoreMeA = "abc";
  
  get() {
    type s = (this & T)
    return {} as { [p in keyof s]: s[p] extends number ? s[p] : never };
  }
}

class B extends A<B> {
  bar = 2;
  ignoreMeB = "xyz";

  init() {
    this.get().bar // has autocomplete for Number object; 
    this.get().ignoreMeA // has no autocomplete
  }
}
  • Please provide additional details in your answer. As it's currently written, it's hard to understand your solution. – Community Sep 06 '21 at 23:57
  • I appreciate this. I probably wasn't clear enough, but my hope was that the autocomplete of **this.get()** would only show **foo** and **bar**. Not that *foo* and **bar** would not have auto complete themselves. so typing: this.get(). <-- that period would then show only **foo** and **bar** as options. – Craig Sep 07 '21 at 02:06