2

I can get the name of an object like so:

let name = obj.prototype.constructor.name;

But what I want to do is get the name of the generic type in my generic Angular service.

@Injectable({
  providedIn: 'root'
})
export class MyService<T> {

    private _value: T;

    constructor(){}

    //... other methods

    private error(error: any)
    {
      console.log(`Error for service type: ${_value.prototype.constructor.name}`)
    }

}

_value.prototype.constructor.name doesn't work, prototype doesn't exist for type T.

I have seen the typescript specific posts that state this is only possible if the constructor takes a parameter of the type because once transpiled type declarations are removed as they don't exist in javascript.

As angular is creating this service via dependency injection what is the proper way to pass the type so it can be instantiated in a way that allows me to read the name as a string?

I have seen in this post: How to get name of generic type T inside service in Angular

It says to use this function:

function createInstance<T>(type: new() => T): T {
    return new type();
}

But I cannot see how I am supposed to use this function in relation to my angular service above.

How do I get the string inside the service?

Guerrilla
  • 13,375
  • 31
  • 109
  • 210

1 Answers1

2

You can't make this work in a type-checked way.
The way this would work is if you could use typeof T.

// doesn't work
@Injectable({
  providedIn: 'root'
})
export class MyService<T> {
  private _value: T;
  private _type: typeof T = T;
                        ^^^^^ 'T' refers to a type but is being used as a value
  private error() {
    console.log(`Error for service type: ${this._type.prototype.constructor.name}`)
  }
}

You can make some adjustments to make this work. Before you continue, realize that you will lose some type checking.

  • replace typeof T with Function (which is basically any for classes)
  • add a constructor argument, for passing the actual type
  • remove the @Injectable() decorator... Angular won't help you find the actual type.

After refactoring:

export class MyService<T> {
  private _value: T;
  private _type: Function;
  constructor(type: Function) {
    this._type = type;
  }
  private error() {
    console.log(`Error for service type: ${this._type.prototype.constructor.name}`)
  }
}

Basic usage looks like this:

let myStringService = new MyService<String>(String);

Just to prove that you lost type checking, this will compile without errors:

class HeckinBamboozled { }
let myNumberService = new MyService<Number>(HeckinBamboozled);

Now you have two options for making this work with DI. The easiest way is to create and provide derived types.

@Injectable({ providedIn: 'root' })
export class MyStringService extends MyService<String> {
  constructor() {
    super(String);
  }
}

This is easily consumable by other services.

@Injectable()
export class OtherService {
  constructor(private stringService: MyStringService) {
  }
}

The second option is much fancier and uses injection tokens.

export const STRING_SERVICE = new InjectionToken<MyService<String>>('My String Service', {
  providedIn: 'root',
  factory: () => new MyService<String>(String)
});

But it's more difficult to consume.

@Injectable()
export class OtherService {
  constructor(@Inject(STRING_SERVICE) private stringService: MyService<String>) {
  }
}

And it's actually less type-safe than the first option. This compiles without errors.

@Injectable()
export class OtherService {
  constructor(@Inject(STRING_SERVICE) private stringService: MyService<HeckinBamboozled>) {
  }
}
Steven Liekens
  • 13,266
  • 8
  • 59
  • 85