A factory function that accepts class method call and returns an instance of this class

Viewed 168

I will start with overview of desired end result I'm trying to achieve:

class Dog {
    public static withName(name: string) {
        this.name = name;
    }
    constructor() {
        //
    }
    //
}

class Cat {
    public static withName(name: string) {
        this.name = name;
    }
    constructor() {
        //
    }
    //
}

function getAnimal(...) { ... }


let dog = getAnimal(Dog.withName('Rex'))
dog.bark();

dog.meow(); // TS Error: method 'meow' does not exist on type 'Dog'

let cat = getAnimal(Cat.withName('Fluffy'))

dog.meow();

dog.bark(); // TS Error: method 'bark' does not exist on type 'Cat'

Of course this is pretty much a simplified almost-pseudo-code. So the idea is next: a function that accepts a class with called static method on it (in order to pre-setup some properties, e.g. withName). Then this function returns an instance of this class with pre-setuped property.

function getAnimal(classWithStaticMethodCall: ???) {
    return new classWithStaticMethodCall()
}

It's not a big deal to achieve something like this:

function foo(bar) {
    return new bar()
}

But the thing is that I want TypeScript to somehow figure out WHAT EXACT CLASS I'm passing and not just return an instance of this class, but the instance of this class with pre-setuped property (with the help of static method withName()).

After trying different things I'm starting to thinking that this is not doable because I'm passing the method and not the class itself (Dog.withName() not Dog). The thing is that I actually saw this API before and now I'm trying to replicate it.

To sum up:

let dog = getAnimal(Dog.withName('Lucky')) // Property name is now set on this exact dog

dog.bark();

dog.name // 'Lucky'

2 Answers

I guess you want something like the following. Basically just accepting a generic function that returns the type. TypeScript will be able to infer the type since it's generic.

Dog

class Dog {
    constructor(
        private mName: string
    ) {
    }

    // function that returns a function
    public static withName(name: string) {
        return () => new Dog(name);
    }

    public Bark() {
        console.info(this.mName + " says 'Bark'.");
    }
}

Cat

class Cat {
    constructor(
        private mName: string
    ) {
    }

    public static withName(name: string) {
        return () => new Cat(name);
    }

    public Meow() {
        console.info(this.mName + " says 'Meow'.");
    }
}

Example

// function that takes a function, specifically one that returns a 'T'
// and then itself returns a 'T', the result of calling that function
function getAnimal<T>(builder: () => T): T {
    return builder();
}

let dog = getAnimal(Dog.withName("Rex"));
dog.Bark();
dog.Meow(); // error Dog cannot Meow

let cat = getAnimal(Cat.withName("Fluffy"));
cat.Bark(); // error Cat cannot Bark
cat.Meow();

Another way you can do this kind of thing is as follows. Basically, you can actually pass the constructor itself as an argument, and type inference will still work. This may be closer to what you were originally looking for.

Dog and Cat

class Dog {
    constructor(private mName: string) { }

    public Bark() {
        console.info(this.mName + " says 'Bark'.");
    }
}

class Cat {
    constructor(private mName: string) { }

    public Meow() {
        console.info(this.mName + " says 'Meow'.");
    }
}

Example 2

// accepting a constructor as an argument, and additional arguments to pass to it
// this is not strictly necessary in the example, this is only a demonstration
// of the concept, which definitely has uses in the right situation
function getAnimal<T>(
    withName: new (name: string) => T,
    name: string
): T {
    return new withName(name);
}

let dog = getAnimal(Dog, "Rex");
dog.Bark();
dog.Meow(); // error Dog cannot Meow

let cat = getAnimal(Cat, "Fluffy");
cat.Bark(); // error Cat cannot Bark
cat.Meow();

First off we need to break down what your code looks like and realize it doesn't do what you think it does.

let dog = getAnimal(Dog.withName('Rex'))

is the same as

const dogWithName = Dog.withName('Rex');
let dog = getAnimal(dogWithName);

In this case dogWithname is calling a function with a void return thus will be undefined

What it appears you want is:

let dog = getAnimal(Dog);
dog.withName('Rex');

but you don't want to have to call .withName. This can be easily accomplished with an interface. You should use an interface because you want each type passed to your factory to have this method so it can be configured and typescript can validate the method exists.

If you don't use an interface and use the code suggested in Dave Cousineau Answer, then there is no point in the getAnimal() method if all it does is call a passed in method (a layer with no additional benefit). Additionally, future programmers won't necessarily call getAnimal() as there is no restriction on the type passed in, but if they do, there is no guarantee they will call .withname() either.

But the thing is that I want TypeScript to somehow figure out WHAT EXACT CLASS I'm passing and not just return an instance of this class, but the instance of this class with pre-setuped property (with the help of static method withName()).

So assuming you don't use a static method, but an instance method with an interface take a look at the following.

interface WithName {
    withName(name: string): void;
}

class Dog 
    implements WithName {
    public name: string;
    
    withName(name: string): void { this.name = name; }
}

class Cat
    implements WithName {
    public description: sring;

    withName(name: string): void { this.description = name;}
}

class AnimalFactory {
    public static getAnimal<TResult extends WithName>(animal: new () => TResult, name: string) : TResult {
        const result = new animal();
        result.withName(name);
        return result;
    }
}

Example usage:

const sparky = AnimalFactory.getAnimal(Dog, 'Sparky');
const mrmeow = AnimalFactory.getAnimal(Cat, 'Mr. Meow');

Each implementation of WithName can do whatever you want with the value, it doesn't have to set 'name' on every type, just accept it as a value. (there is no problem with it doing the same thing, but then I'd wonder why not use an abstract class instead with a required super() call?

public abstract class WithName {
  constructor(public name: string) { }
}

public Dog extends WithName {
  constructor(name: string) { 
    super(name);
  }
}

public Cat extends WithName {
  constructor(name: string) { 
    super(name);
  }
}

It just seems the GetAnimal() method is superfluous.

Related