Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Factory returning classes in TypeScript

Wrote a library that, depending on the input data; creates classes and uses them inside a main class.

To make the code more maintainable and readable, I've moved the class generation logic into a separate file that exports a factory function.

Code was written in ES2015. Now I'm migrating to TypeScript.

Here is a pseudo example:

factory.ts

export default function (foo:string) => {
    class A {
        value:string = foo + '-A';
    }

    return { A };
};

Main.ts

import factory from './factory';

export default class Main {
    private inner:any;
    constructor(foo:string) {
        this.inner = factory(foo);
    }
    get a() {
        return new this.inner.A();
    }
}

Usage:

let main = new Main('bar');
console.log(main.a.value); // "bar-A"

Problems:

  • TS compiler error:
    Default export of the module has or is using private name 'A'.
  • Cannot define the type of the getter a as A in Main class (e.g. get a():A { ... }

How would you resolve this (keeping the factory classes in a separate file)? Or should I change the design pattern?

like image 908
Onur Yıldırım Avatar asked Mar 06 '17 19:03

Onur Yıldırım


2 Answers

How about something like:

export interface Base {}

export interface IA extends Base {
    value: string;
}

export type Builders = {
    [name: string]: { new <T extends Base>(): T };
}

export function factory(foo: string): Builders {
    class A implements IA {
        value:string = foo + '-A';
    }

    return { A };
};

And:

import { factory, IA, Builders } from './Factory';

export default class Main {
    private inner: Builders;

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

    get a():IA {
        return new this.inner.A() as IA;
    }
}

Edit

What's wrong with this as factory.ts:

export class Base {}

export type Builders = {
    [name: string]: { new <T extends Base>(): T };
}

class A extends Base {
    value: string;

    constructor();
    constructor(foo: string);
    constructor(foo?: string) {
        super();
        this.value = foo + "-A";
    }
}

// more classes...

export function factory(foo: string): Builders {
    return { A: A.bind(A, foo) };
};

It's basically the same as what you did, just that the classes are not defined inside the class, and are exported so no need for the interfaces I suggested.
Also, this way all the classes will only be evaluated once and not every time the factory function is invoked.

like image 200
Nitzan Tomer Avatar answered Nov 19 '22 06:11

Nitzan Tomer


Your code (the OP's) is working just fine in July 2019 with Typescript 3.5.2.

PS. I got here when I was trying to create a Class Factory in Typescript.

Things to keep in mind:

  1. The concept of Class definition in Typescript has two folds. One is the instance type, and the other one is the constructor function. (source in TS Docs)
  2. Since Typescript 2.8 released in March 2018, you can explicitly create the instance type from the constructor function using InstanceType. e.g. type classType = InstanceType<typeof C>. This should make the explicit type definition in the selected answer simpler.
  3. Finally, I got my code working using a simple trick as following:
export default class MyNewClass
   extends ClassFactory("custom param1") {}

This way I am actually defining the generated class as a new class by itself, so both the constructor function and the instance-type type are going to be implicitly created and will be valid. And I don't need to explicitly define the two separately.

like image 45
Aidin Avatar answered Nov 19 '22 06:11

Aidin