MarkM MarkM - 24 days ago 5x
TypeScript Question

Strategy for generalizing and DRYing services

I am working on a sprawling Angular 2 project that involves pulling several different types of objects from a noSQL database. These objects are represented with simple model classes. I'm using services to pull the information off the DB and instantiate objects which are passed off to the components in much the same way the Heroes tutorial works.

The problem I'm having is that for each type of object, I have a service that is just a little different than the others -- the primary difference is I need the service to instantiate and return different types of objects. In an attempt to consolidate these services I tried passing the type of object from the component to the service where the service would make the DB call and instantiate the object of the type passed.

It Looks like this:

In the component:

import { Market } from './market'; // Market is just a simple model class

/* clipped stuff */

this.route.params.forEach(params => {
this.nodeID = params['id'];
this.nodeService.getNodeOfType(Market, this.nodeID)
.subscribe(market => = market)


export class NodeService {
getNodeOfType(type, nodeID: number) {
return this.neo4j.readProperties(nodeID)
.map(properties => new type(nodeID, properties));

I have two questions:

  1. When I pass a class type as a parameter, how do I type that in the function parameters?

  2. I don't have much Typescript experience and haven't seen anything like this in docs, which makes me suspect it might be an anti-pattern of some sort. Is this a poor design practice for some reason I'll find out later?


One big anti-pattern stands out: You're losing all type safety here by not declaring a return type on your getNodeOfType() method. The need to have a method return different types based on some input parameter is the classic use case for a generic method - which Typescript does support.

Here's one possible way of implementing it in your case:

getNodeOfType<T>(factory: (input:any, nodeId:number)=>T , nodeID: number): T {
  return this.neo4j.readProperties(nodeID)
                   .map(properties => factory(properties, nodeId));

First, note that rather than passing in a type, you're passing in a function that returns an object of the type you want (T) given some input. Here, that input is type any, but if there is some base type that you know this.neo4j.readProperties() will always return, it would be better to replace any by that type.

More importantly, note that your function is now returning type T, where T is determined on the fly based on the return type of the factory function you pass it, so you don't lose type safety.

In response to your comment:

it seems that there's no way to avoid creating a factory function for each type of object I want to use. So I still end up with a fair amount of boilerplate if I have a lot of different types, right?

Not necessarily - when I use this pattern, the factory functions I pass in are almost invariably simple anonymous functions. Consider the trivial example of:

class Foo{ 
  constructor(input: any, id:number) {/*constructor stuff*/ }

foo: Foo = myService.getNodeOfType((props, id)=>new Foo(props, id), 1234);

The utility of having your service method take a factory function rather than a type is that it gives you more flexibility in mapping the retrieved data to your desired result type (e.g. if you want it to return an interface or a class whose constructor has a different signature, or if you want to do input validation before passing it to a constructor).

I should've noted previously, though, that if you know you won't need this flexibility and will always be simply passing the inputs to constructors with this same signature, you can do the following to achieve what you were going for initially.

getNodeOfType<T>(Type: new(input:any, id:number)=>T , nodeID: number): T {
  return this.neo4j.readProperties(nodeID)
   .map(properties => new Type(properties, id));

//usage (same Foo class as above)
foo: Foo = myService.getNodeOfType(Foo, 1234);

Note the type of the first parameter - it looks awkward until you break it down: It expects something with a constructor that takes two an any and a number as args and returns an instance of type T.