Search code examples
javascripttypescriptgenericsgeneric-programming

Detecting whether a generic type argument has an id property?


I'm designing a generic Slice<E> class which represents a slice of instances of a set of instances. For example if we have Todo instances, then the slice could represent all the ones that are completed.

If the Todo class has an id property we would like to set hasID to true on the Slice instance, such that we can also index the Slice instance by id. Is there a way to detect at runtime whether the generic argument has and id property?


Solution

  • There's not a lot here to go on without code.

    On the face of it: no, you cannot possibly analyze a generic type argument at runtime since the type system is erased. At runtime you can only analyze runtime values, such as the actual objects you pass in to the constructor. At compile time you can probably make the compiler give a specific true or false boolean literal type to hasID based on the generic type parameter. And you can do both of those things and hope that you've got a solution where the runtime values actually match up with compile-time types.

    Let's try it. Here's a sketch of a possible solution:

    class Slice<E> {
      instances: E[]
      hasID: E extends {id: any} ? true : false;
      constructor(firstInstance: E, ...restInstances: E[]) {
        this.hasID = 'id' in firstInstance as Slice<E>['hasID'];    
        this.instances = [firstInstance, ...restInstances];
      }
    }
    

    The hasID property is given a conditional type that evaluates the E type parameter and returns true if E['id'] exists, and false otherwise.

    The constructor accepts at least one parameter of type E, the first one of which is analyzed at runtime for an id property in order to set the hasID runtime value.

    Let's see if it works:

    const sliceOne = new Slice({a: 1}, {a: 2});
    sliceOne.hasID // false at compile time and runtime
    
    const sliceTwo = new Slice({id: 1}, {id: 2});
    sliceTwo.hasID // true at compile time and runtime
    

    Looks good. Still there are edge cases you might need to worry about, like:

    declare const notSure: object;
    const oopsie = new Slice(notSure);
    oopsie.hasID; // false at compile time, maybe true at runtime!
    

    The compiler can't verify that object has an id property, so it gives hasID the type false. But of course an object may have an id property, so maybe hasID will be true at runtime. Is there a way to deal with this? Maybe. But it's not straightforward. The question is how likely you are to run into these cases and if you care about them.

    Anyway, hope that makes sense and gives you some direction. Good luck!