Search code examples
typescriptmapped-types

Typescript how to make a mapped type extends typeof Class


Typescript mapped types syntax is really obscure to me, but I will try to make myself clear.

Basically I want a type filter to return me only the keys of an object that have a type of an Class or an array of classes. After some tries, I ended up with this:

type OnlyObjectProperties<T> = {
  [key in keyof T]: T[key] extends object ? key : never;
}[keyof T];

export type ClassRelation<T> = {
  [key in keyof Pick<T, OnlyObjectProperties<T>>]: string;
};

But I want the OnlyObjectProperties<T> to only return the keys where the type is an class.

For example:

class Person{
    name: string;
    task: Task // consider that Task class is defined.
}

const test: ClassRelation<Person> = {tasks: 'tasks'} // only the task key should appear and be allowed on the test variable 

I already tried something like this:

type OnlyObjectProperties<T> = {
  [key in keyof T]: T[key] extends new(...args: any[]) => any? key : never;
}[keyof T];

But without success.

To summarize, I want the keys of a given type, that have a type of a Class

This is the stackblitz I've been using to prototype https://stackblitz.com/edit/ts-node-gckhuf?file=index.ts


Solution

  • You can't really distinguish an instance of a class from any other object in TypeScript or JavaScript. All objects have a constructor property, even objects provided natively like Date. Instead, you need to find some way to identify the class instances you'd like to preserve, and to distinguish them structurally from other object types.

    The example classes you gave look like this:

    class User {
      id!: number;
      createdAt!: Date;
      tasks!: Task[];
    }
    class Task {
      id!: number;
      doneAt!: Date;
      user!: User
    }
    

    both of which have an id property of type number. If that's a good way to tell good class instances apart from bad ones, then you can just change your type function accordingly:

    export type ClassRelation<T> = {
      [K in keyof Pick<T, OnlyPropertiesWithANumericId<T>>]: string;
    };
    
    type OnlyPropertiesWithANumericId<T> = {
      [K in keyof T]: T[K] extends { id: number } ? K : never;
    }[keyof T];
    

    And you can verify that it works as intended:

    const onlyRelationsFromTask: ClassRelation<Task> = {
      user: "user"
    }
    

    Playground link to code