skip to Main Content

Consider the following TypeScript classes:

class A {

  foo: string;

  constructor({ foo }: { foo: string }) {
    this.foo = foo;
  }

  equals(other: A) {
    return this.foo === other.foo;
  }
}


class B extends A {

  bar: number;

  constructor(b: {
    foo: string;
    bar: number;
  }) {
    super({ foo: b.foo });
    this.bar = b.bar;
  }
}

const b = new B({
  foo: 'Test',
  bar: 42,
});

In reality, class B has about 20 properties which makes the class and especially the constructor definition super verbose because every property is repeated three separate times. How could I make it more compact?

You can’t do constructor({ foo, bar }: B) because B also requires the object to have things like equals().

I also tried the following but same result:

constructor(b: { [K in keyof B]: B[K] }) {   
  super({ foo: b.foo });
  .
  .
  .
Argument of type '{ foo: string; bar: number; }' is missing the following properties from type '{ [K in keyof B]: B[K] }': equals.ts(2345) 

3

Answers


  1. There’s a couple different approaches you can take.

    Introspect the classes themselves

    Given a couple helper types, you can do

    type NonFunctionFieldNamesOfType<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
    type NonFunctionFields<Tc> = Pick<Tc, NonFunctionFieldNamesOfType<Tc>>;
    
    class A {
      foo: string;
    
      constructor({ foo }: NonFunctionFields<A>) {
        this.foo = foo;
      }
    
      equals(other: A) {
        return this.foo === other.foo;
      }
    }
    
    class B extends A {
      bar: number;
    
      constructor(b: NonFunctionFields<B>) {
        super({ foo: b.foo });
        this.bar = b.bar;
      }
    }
    
    const b = new B({
      foo: "Test",
      bar: 42,
    });
    

    You can of course also limit those types using Pick and Omit if required.

    Using ConstructorParameters

    You can also use ConstructorParameters to introspect the superclass’s constructor’s parameters, and destructuring in the subclass’s constructor.

    This could be useful for more complex cases.

    With ConstructorParameters, you could at least get to

      foo: string;
    
      constructor({ foo }: { foo: string }) {
        this.foo = foo;
      }
    
      equals(other: A) {
        return this.foo === other.foo;
      }
    }
    
    class B extends A {
      bar: number;
    
      constructor({ bar, ...a }: ConstructorParameters<typeof A>[0] & { bar: number }) {
        super(a);
        this.bar = bar;
      }
    }
    
    const b = new B({
      foo: "Test",
      bar: 42,
    });
    
    Login or Signup to reply.
  2. As an alternative, if your not creating 1000’s of the same instance of a class, you might find not using classes would actually work better. I find Typescript much easier keeping things functional. Due to Javascript scope / closures etc, I find JS classes are rarely needed.

    With that in mind, your above could be simplified to something like ->

    type AProps = {
      foo: string;
    }
    
    function A(args: AProps) {
      return { 
        args,
        equals: (p:AProps) => p.foo === args.foo
      }
    }
    
    type BProps = {
      bar: string;
    }
    
    function B(args: BProps & AProps) {
      const a = A(args);
      return {...a, args};
    }
    
    const objA = A({foo: 'fooA'});
    
    const objB = B({foo: 'fooB', bar: 'barB'});
    
    const objC = B({foo: 'fooA', bar: 'barC'})
    
    console.log(objA.equals(objB.args)); //false
    console.log(objC.equals(objA.args)); //true
    console.log(objA.equals(objC.args)); //true
    

    TS Playground

    Another advantage here of going functional, is that you get the massive added bonus of composition. IOW: your not bounded by Javascripts OOP single inheritance model. Of course the downside is that all typing is duck typing, eg. You couldn’t for instance use instanceOf etc. You would be required to do -> if ('foo' in ... etc.

    Login or Signup to reply.
  3. To avoid writing dozens of unnecessary lines, you do it like this:

    type AProps = {
        foo: string
    }
    
    class A {
        data: AProps;
        constructor(data: AProps) {
            this.data = data;
        }
    }
    
    type BProps = AProps & {
        bar: string
    }
    
    class B {
        data: BProps;
        constructor(data: BProps) {
            this.data = data;
        }
    }
    
    let a = new A({foo: "fooa"});
    let b = new B({foo: "foob", bar: "bar"});
    
    console.log(a.data.foo, b.data.foo, b.data.bar);
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search