cubabit
cubabit

Reputation: 2700

Is there a cleaner way than this of defining constructor arguments and instance properties of a class in Typescript?

Currently I am defining the constructor args and instance properties of my Typescript classes like this:

interface ConstructorProps {
  foo: number;
  bar: string;
  baz: boolean;
}

class MyClass {
  foo: number;
  bar: string;
  baz: boolean;

  constructor (props: ConstructorProps) {
    this.foo = props.foo;
    this.bar = props.bar;
    this.baz = props.baz;
  }
}

Which is quite repetitive. Is there a way of doing this in a cleaner way, even if it just for the interface and instance properties?

Upvotes: 2

Views: 111

Answers (3)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249466

Using mapped types and conditional types we can create a type that contains all the fields of a class and use it as a constructor argument. Also we can use Object.assign to copy the properties instead of assigning each field:

type NotMethods<T> = { [P in keyof T]: T[P] extends (...args: any[]) => any ? never: P }[keyof T];
type Fields<T> = { [P in NotMethods<T>]: T[P] }

class MyClass {
    foo: number;
    bar: string;
    baz: boolean;

    constructor(props: Fields<MyClass>) {
        Object.assign(this, props)
    }


    method() {

    }
}

new MyClass({
    foo: 1,
    bar: '',
    baz: true
});

Playground link

The Fields type is reusable for any class. All fields of the class will all be required in the constructor argument.

Or if you have optional fields and are using strict null checks, you can write a version that preserves the optionality of the field in the constructor parameter:

type NotMethods<T> = Exclude<{ [P in keyof T]: T[P] extends (...args: any[]) => any ? never: P }[keyof T], undefined>;
type FilterUndefined<T, TKeys extends keyof T = keyof T> = Exclude<{ [P in TKeys]: undefined extends T[P] ? never: P }[TKeys], undefined>;
type KeepUndefined<T, TKeys extends keyof T = keyof T> = Exclude<TKeys, FilterUndefined<T, TKeys> | undefined>

type Fields<T> = { [P in FilterUndefined<T, NotMethods<T>>]: T[P] } & { [P in KeepUndefined<T, NotMethods<T>>]?: T[P] }

class MyClass {
    foo: number;
    bar: string;
    baz?: boolean;

    constructor(props: Fields<MyClass>) {
        Object.assign(this, props)
        props.foo
    }

    method() {

    }
}

new MyClass({
    foo: 1,
    bar: '',
    baz: true
});

new MyClass({
    foo: 1,
    bar: '',
});

Playground link

Upvotes: 1

Mohamed Ramrami
Mohamed Ramrami

Reputation: 12691

You can also consider the builder pattern :

1. Create the builder type and createBuilder util function :

type Builder<T> = {
  [k in keyof T]: (arg: T[k]) => Builder<T>
} & { build(): T }

function createBuilder<T>(type: { new(...args: any[]) : T}): Builder<T> {
    var built: any = {};
    var builder = new Proxy({}, {
        get: function(target, prop, receiver) {
             if (prop === 'build') {
                  return () => new type(built);
             }

             return (x: any): any => {
                  built[prop] = x;
                  return builder;
             }
         }
    });
    return builder as any;
}

2: Use it in your Class :

class MyClass {
    foo: number;
    bar: string;
    baz: boolean;

    public static createBuilder(): Builder<MyClass> {
         return createBuilder<MyClass>(MyClass);
    }

    constructor (builder: Builder<MyClass>) {
        Object.assign(this, builder);
    }
}

3: Use your class :

const me = MyClass.createBuilder()
    .bar("bar")
    .baz(true)
    .foo(3)
    .build();

console.log(me);

Problems :

  • I could not make it work with private fields
  • Depends on the Proxy object

Credits:

Upvotes: 0

Jonathan
Jonathan

Reputation: 9151

This might not work for all scenarios, but this compiles:

class MyClass {
  foo: number;
  bar: string;
  baz: boolean;

  constructor (props: MyClass) {
    this.foo = props.foo;
    this.bar = props.bar;
    this.baz = props.baz;
  }
}

const props = {
    foo: 1,
    bar: "s",
    baz: true
};

const x = new MyClass(props);

Upvotes: 0

Related Questions