Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get all the properties (but not methods) from a class instance?

Tags:

typescript

I want to create a derived from a class object type, which has all the properties from the class instance. Simply put, I need a type of the result of spreading class instance to a plain object.

Just like this:

class Foobar {
    foo: number = 0;

    bar(): void {} 
}

type ClassProperties<C extends new(...args: readonly unknown[]) => unknown> =
    C extends new(...args: readonly unknown[]) => infer R 
        ? { [K in keyof R]: R[K] } 
        : never
;

const foobar = new Foobar();
const data: ClassProperties<typeof Foobar> = { ...foobar };

But the example above doesn't work, TS says Property 'bar' is missing in type '{ foo: number; }' but required in type '{ foo: number; bar: () => void; }'.

I'm confused a little, it seems to me as a very simple and regular task. Is there a good and reliable solution?

Thank you.

like image 241
shau-kote Avatar asked Oct 21 '25 06:10

shau-kote


2 Answers

You need to map over each property separately and do something different conditionally with each of those properties. But your only mapped type:

{ [K in keyof R]: R[K] } 

Treats all properties identically.


You can take advantage of key renaming in a mapped type to conditionally map the function properties to never, which removes them from the resulting type.

type Newable = { new(...args: readonly unknown[]): unknown }
type AnyFn = (...args: unknown[]) => unknown

type ClassProperties<C extends Newable> = {
    [
        K in keyof InstanceType<C>
            as InstanceType<C>[K] extends AnyFn
                ? never
                : K
    ]: InstanceType<C>[K]
}

First, notice I abstracted Newable and AnyFn to their own types to keep this readable. And your use of infer is covered by the built in InstanceType.

Now for the mapped type, it starts my iterating over all the keys of the instances of the class. We use as to change the property name. That new property name is a conditional type that checks to see if the property is a function, which it then maps to never, or other wise leaves the key unchanged.

The value of each mapped property is the same as it would have been on the instance. In other words the value type is unchanged.

This now does what you want:

class Foobar {
    foo: number = 0;
    bar(): void {} 
}

type Test = ClassProperties<typeof Foobar>
// { foo: number }

const foobar = new Foobar();
const data: ClassProperties<typeof Foobar> = { ...foobar }; // fine

See playground


However, your type only deals with the instance type, so you can make this much simpler if you pass that in directly:

type ClassProperties<C> = {
    [K in keyof C as C[K] extends AnyFn ? never : K]: C[K]
}

class Foobar {
    foo: number = 0;
    bar(): number {return 123} 
}

type Test = ClassProperties<Foobar>
// { foo: number }

const foobar = new Foobar();
const data: ClassProperties<Foobar> = { ...foobar }; // fine

See playground

like image 161
Alex Wayne Avatar answered Oct 23 '25 21:10

Alex Wayne


Would like to update last type from Alex Wayne's answer using global type Function, because it works better with class methods and more concisely:

type ClassProperties<C> = {
    [K in keyof C as C[K] extends Function ? never : K]: C[K]
}
like image 41
Mark X Avatar answered Oct 23 '25 20:10

Mark X