mikeysee
mikeysee

Reputation: 1763

How to create a type that returns and object with classes excluded

Given the following:

class MyClass {}
class MyOtherClass { something!: number; }

type HasClasses = {
    foo: MyClass;
    bar: string;
    doo: MyClass;
    coo: {x: string;};
    boo: MyOtherClass;
};

type RemovedClasses = RemoveClassTypes<HasClasses>;

I want RemovedClasses to equal { bar: string; coo: {x: string;} }. How can I do that?

I have tried what I thought would work but it didnt for some reason:

type ClassesKeys<T> = {
  [P in keyof T]: T[P] extends new () => any ? P : never;
}[keyof T]

type RemoveClassTypes<T> = Omit<T, ClassesKeys<T>>;

Upvotes: 1

Views: 49

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1075249

You're saying you want to remove properties from the type based on whether they relate to classes, leaving non-class object types. That's why in your example you're removing foo and doo (both MyClass) and boo (MyOtherClass), but not coo ({x: string;}).

I don't think you can.

TypeScript's type system is structural (based on the shape of things), not nominal (based on their names). An object is just an object, it doesn't say whether it was created via new X() or Object.create() or an object literal — not at the type level, and largely not at the runtime JavaScript level either. What matters is the properties and methods that it has; its shape.

For example, look at this:

class Example {
    something: string;
    constructor(s: string) {
        this.something = s;
    }
    method(): void {
    }
}

let ex1 = new Example("ex1");
let ex2 = {
    something: "ex2",
    method(): void {
    }
};

From the type perspective, there's no difference between ex1 and ex2, even though at a runtime perspective there is a minor difference (ex1 inherits its method, while ex2 has it as an own property).

Now assume you have these declarations:

let ex3: Example;
let ex4: { something: string; method(): void; };

All of these assignments are valid:

ex3 = ex1;  // Works
ex3 = ex2;  // Works
ex4 = ex1;  // Works
ex4 = ex2;  // Works
ex1 = ex4;  // Works
ex2 = ex4;  // Works

The types of those things are all compatible. The instance type Example declared via a class construct, the type inferred from the object initializer for ex2, and the inline type assigned to ex4, they're all compatible with each other.

One red herring would be that objects from classes have an inherited constructor property, maybe we can use that! But you can't, because even ex2 above has an inherited constructor property (from Object.prototype.constructor, which is Object), and separately I think TypeScript treats the constructor property a bit specially (but can't point to any docs on that and may be mistaken).

So I'm afraid I don't think you can do what you've said you want to do.

Upvotes: 1

Related Questions