3

I'm trying to write a generic class that is passed a key key corresponding to a key of one of a set of known interfaces on construction and that can later be passed an object thing and type-safely access thing[key]. This is what I've got:

interface AB {
  a: string
  b: number
}

interface BC {
  b: number
  c: Date
}

type ABC = AB | BC

class Getter<T extends ABC> {
  key: keyof T;

  constructor(key: keyof T) {
    this.key = key;
  }

  get(thing: T): string {
    const value = thing[this.key];
    return value.toString();
  }
  
}

const getter = new Getter<AB>('b');

Playground Link

Here, I'd expect Typescript to infer that because T extends ABC that T[keyof T] = AB[keyof AB] | BC [keyof BC] = string | number | date. However, it seems to get stuck at T[keyof T]. Even adding an as AB[keyof AB] | BC[keyof BC] to that line doesn't fix it, I need as unknown as AB[keyof AB] | BC[keyof BC]! Is there any way to get this working without that line?

Also, is there any way I could parameterize over the value of key as a type instead of parameterizing over the type of thing?

Joel Newman
  • 125
  • 7
  • 1
    Does this answer your question? [Property 'toLowerCase' does not exist on type 'T\[keyof T\]'](https://stackoverflow.com/questions/67901487/property-tolowercase-does-not-exist-on-type-tkeyof-t) – Erik Philips Jun 10 '22 at 00:45

3 Answers3

1

When T extends ABC, it can have many other properties and types. Which can therefore not have .toString() method.

type G = {
  a: string;
  b: number;
  hello: undefined;
};

const g = new Getter<G, keyof G>("hello");

g.get({
  a: "a",
  b: 1,
  hello: undefined
}); // undefined does not have .toString() method

Playground Link

ghybs
  • 47,565
  • 6
  • 74
  • 99
  • oh huh! is there any way to achieve what I was going for typesafely then? Is there any way to restrict T to be either AB or BC? Or maybe just that all of its values need to implement the interface `{ toString: () => string}`? – Joel Newman Jun 10 '22 at 16:39
1

Is there any way I could parameterize over the value of key as a type instead of parameterizing over the type of thing?

Yes, nothing easier than that:

class Getter<Key extends string | symbol> {
  key: Key;

  constructor(key: Key) {
    this.key = key;
  }

  get(thing: {[k in Key]: string | number | Date}): string {
    const value = thing[this.key];
    return value.toString();
  }
}

(Playground demo)

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
0

This is simply a limitation of the nature of typescript. You need to check members, then do a typed cast at runtime. Hope that helps!

For example:

if (AnimalObject.Object.Keys().Includes('Name')) {
  // We now know the generic type has a matching member
}

As for an implementation, I was would make a list of interfaces that 'T' can be then make an array of members for each interface, following the above logic you may end up with something like:

// Store your valid types for checking
const ValidInterfacesSet: any[] = {Dog,Cat,car,Mammal};

//Look through members and confirm them... 
GetAndCastType(typeToCheck: any) {
    typeToCheckKeys: string = typeToCheck.Object.keys();
    for (inter: any of ValidInterfacesSet) {
        const interMemberNames: string[] = inter.object.keys();
        
        if(array1.sort().join(',')=== array2.sort().join(',')) {
          // Returning the casted type if the iter members contain the same as typeToCheckMembers
             return typeToCheck as inter; 
        }
    }
}
  • Thanks, but what do you mean by check members and do a typed cast? – Joel Newman Jun 09 '22 at 23:26
  • For example if you have an interface 'Animals' and it contains members 'Name' and 'Type'. You can generically confirm these members and then execute a case to the animal interface. if (AnimalObject.Object.Keys().includes('Name')) { (I'll add more details in the answer for you) – Jamie Nicholl-Shelley Jun 10 '22 at 00:32