I have a generic function that I would like to get the name of the class that is passed in.
public addComponent<T extends Component>(): Component {
comp = new Component() as T;
comp.name = T.constructor.name;
console.log(comp.name);
return comp;
}
Then lets say I call it like so:
obj.addComponent<MyClass>();
I would then expect the log to display "MyClass". But currently I get an error saying:
Cannot find name 'T'.
There's no way to do that.
The T doesn't exist at runtime, it's only for compilation and the compiler removes that (along with the types) so the resulting javascript for this:
class MyFactory {
public addComponent<T extends Component>(): Component {
let comp = new Component() as T;
comp.name = T.constructor.name;
console.log(comp.name);
return comp;
}
}
Is:
var MyFactory = (function () {
function MyClass() {
}
MyFactory.prototype.addComponent = function () {
var comp = new Component();
comp.name = T.constructor.name;
console.log(comp.name);
return comp;
};
return MyFactory;
}());
As you can see, the js code doesn't have the generics signature, so there's no definition for T, and so T.constructor results in the error you're receiving.
If you want the method to create an instance of a class by passing it then it should look like:
interface ComponentConstructor<T extends Component> {
new(): T;
name: string;
}
class Component {
name: string;
}
class MyComponent extends Component {}
class MyFactory {
public addComponent<T extends Component>(ctor: ComponentConstructor<T>): Component {
let comp = new ctor();
comp.name = ctor.name;
console.log(comp.name);
return comp;
}
}
let factory = new MyFactory();
let obj = factory.addComponent(MyComponent as ComponentConstructor<MyComponent>);
(code in playground)
Related
I'm trying to reproduce the #Getters of the common Java library Lombok. The result would be :
#Getters()
export class Example {
private property: string;
}
const test = new Example();
console.log(test.getProperty());
To do this, I try to change the js class definition by override it with a TS Decorator:
import { capitalize } from "lodash";
export const Getters = () => <T extends {new(...args:any[]):{}}>(constructor:T) => {
return class extends constructor {
constructor(...args: any[]) {
super(...args);
const props = Reflect.ownKeys(this);
props.forEach((prop: string) => {
const capitalizedKey = capitalize(prop);
const methodName = `get${capitalizedKey}`;
Reflect.defineProperty(this, methodName, { value: () => this[prop], configurable: false, enumerable: false });
});
}
}
}
For the moment, I have this error The X property does not exist on the X type.
But I can have the result with this:
console.log(test['getProperty']())
Doe's someone know how to change the class definition with TS decorator pls?
Playground: https://typescript-lodash-playground-1fuxpz.stackblitz.io
As mentioned in the documentation. Decorators do not change the type: https://www.typescriptlang.org/docs/handbook/decorators.html#class-decorators
TypeScript is not Java, you don't use getter methods and don't need to add #Getters. The idiomatic way to achieve this is to simply create a public property that can't be written to:
export class Example {
readonly property: string;
constructor(p: string) {
this.property = p;
}
}
or short
export class Example {
constructor(readonly property: string) {}
}
then use
const test = new Example("demo");
console.log(test.property);
I'm developing an Entity-Component-System in TypeScript where entities contain a map of their components. CT stands for ComponentType Here is the code:
class Entity {
components: Map<CT, Component>;
constructor() {
this.components = new Map();
}
get(componentType: CT): Component {
return this.components.get(componentType);
}
}
const enum CT {
Position,
Health // etc..
}
class Component {
type: CT;
constructor(type: CT) {
this.type = type;
}
}
class HealthComponent extends Component {
amount: number;
constructor() {
super(CT.Health);
this.amount = 0;
}
}
The problem is that when I do something like:
let healthComponent = new HealthComponent();
healthComponent.amount = 100;
let entity = new Entity();
entity.components.set(healthComponent.type, healthComponent);
let position = entity.get(CT.Health);
console.log(health.amount);
I get this error: Property 'amount' does not exist on type 'Component'
If I change the Entity.get function to instead return any like this, then the error goes away:
get(componentType: CT): any {
return this.components.get(componentType);
}
But then I no longer get code-completion, which I would like.
Is it possible to have it return the proper type, so I can have the code-completion and error-detection?
class Component {
// error: 'amount' does not exist on type 'Component'
amount: number; // <-- NOTE: moved amount here from HealthComponent
type: CT;
constructor(type: CT) {
this.type = type;
}
}
class HealthComponent extends Component {
// removed amount from here
constructor() {
super(CT.Health);
this.amount = 0;
}
}
I managed to hack something together using TypeScript's conditional types. It's not ideal but it might work for you:
Let's declare the enum and component classes first:
const enum CT {
Position,
Health
}
class HealthComponent {
amount: number;
constructor() {
this.amount = 0;
}
}
class PositionComponent {
position: number;
constructor() {
this.position = 1;
}
}
Then the conditional type that maps the enum values to component classes:
type TypeMapper<T extends CT> = T extends CT.Position
? PositionComponent
: HealthComponent;
It's a generic type that based on it's type parameter T either evaluates to the PositionComponent or the HealthComponent class.
And now the entity class:
class Entity {
private components: Map<CT, TypeMapper<CT>>;
constructor() {
this.components = new Map();
}
set<T extends CT>(ct: T, component: TypeMapper<T>) {
this.components.set(ct, component);
}
get<T extends CT>(ct: T): TypeMapper<T> {
return this.components.get(ct) as any; //a little lie here to shut up the TS compiler
}
}
Now when you instantiate everything, the types should align for you correctly:
const entity = new Entity();
entity.set(CT.Health, new HealthComponent());
const health = entity.get(CT.Health);
health.amount //and the amount prop should show up in intellisense correctly.
As I said, it's not ideal because it relies on using any, also depending on the number of component classes you have, your TypeMapper type can become huge, but at least the Component base class and super() calls to it's constructor are no longer necessary so you win some lines of code there.
Here's the TS playground link.
I want to write a simple function in the child class that only returns it's own keys (and not the parent).
class Parent{
protected _parentAttribute!: string;
constructor() {
this._parentAttribute='test';
}
}
class Child extends Parent{
childAttribute!: string;
constructor() {
super();
console.log("My unique child keys are:", Object.keys(this));
}
}
let child=new Child();
Result:
My unique child keys are: [_parentAttribute,childAttribute]
Desired result: My unique child keys are: [childAttribute]
Is this possible?
First, create a variable at the top class and then in that variable, store the keys which are in the top class. Use a filter function inside the child class to filter the top variables. There's nothing bad in this approach as I think. The filter should work fine and this method should work every time.
class Parent{
protected _parentAttribute: string;
protected topKeys;
constructor() {
this._parentAttribute='test';
this.topKeys = 'test' // asign something it so it comes in your property names
let somevar = Object.getOwnPropertyNames(this) // get all the properties
this.topKeys = somevar // put them in this variable
}
}
class Child extends Parent{
public childAttribute: string;
constructor() {
super();
this.childAttribute = 'test'
let keyofChild = Object.keys(this).filter(keys => !this.topKeys.includes(keys))
console.log("My unique child keys are:", keyofChild); // childAttribute
}
}
let child = new Child();
Ended up getting it like this. However, feels hacky and I'm open to better answers:
class Parent{
protected _parentAttribute: string;
protected _parentAttribute2: string;
protected _parentKeyList: Array<string>;
constructor() {
this._parentAttribute='test';
this._parentAttribute2='another value';
this._parentKeyList=['']; //Oddly necessary...
this._parentKeyList=Object.keys(this); //Must be at the end of constructor
}
class Child extends Parent{
childAttribute: string;
constructor() {
super();
const uniqueKeys=_.difference(Object.keys(this),this._parentAttributes); //Using lodash
console.log("My unique child keys are:", uniqueKeys);
}
let child=new Child();
after super() the child is equal to the parent as are its properties
TYPESCRIPT:
class Parent {
_parentAttribute1: string = "test"
_parentAttribute2 = 'test';
constructor() {
}
}
class Child extends Parent {
getParentPropertyNames(): Array<string>{
delete this.parentPropertyNames;
return Object.getOwnPropertyNames(this)
}
// this is the magic as it get called immediatly after super()
parentPropertyNames: Array<string> = this.getParentPropertyNames()
// </magic>
childAttribute1 = 'test';
childPropertyNames!: string[]
constructor() {
super();
}
get uniqueNames(){
this.childPropertyNames = Object.getOwnPropertyNames(this)
//#ts-ignore
.filter(name => !this.parentPropertyNames.includes(name)) // wastefull computation
console.log(this.childPropertyNames)
return this.childPropertyNames
}
}
let child = new Child();
child.uniqueNames
parsed for running on SO
class Parent {
constructor() {
this._parentAttribute1 = "test";
this._parentAttribute2 = 'test';
}
}
class Child extends Parent {
constructor() {
super();
// this is the magic as it get called immediatly after super()
this.parentPropertyNames = this.getParentPropertyNames();
// </magic>
this.childAttribute1 = 'test';
}
getParentPropertyNames() {
delete this.parentPropertyNames;
return Object.getOwnPropertyNames(this);
}
get uniqueNames() {
this.childPropertyNames = Object.getOwnPropertyNames(this)
//#ts-ignore
.filter(name => !this.parentPropertyNames.includes(name)); // wastefull computation
console.log(this.childPropertyNames);
return this.childPropertyNames;
}
}
let child = new Child();
child.uniqueNames;
Using ES6 is there a way to apply multiple mixins which are defined in an array? The mixins would be defined as such:
const mixins = Array('Mixin', 'Mixin2');
Then creating a mixin with:
export const Mixin = function (superClass) {
return class extends superClass {}
And using the mixin with:
export class MyClass extends MultipleMixins(BaseClass)
You can use reduce() over the array of mixins, pass in a base class and keep returning a new mixed class. This will just apply all the mixing in order:
class BaseClass {
constructor(name) {
this.name = name
}
}
// adds an uppercase
const Mixin = function(superClass) {
return class extends superClass {
get uppercase() {
this.name = this.name.toUpperCase()
return this
}
}
}
//adds a reverse
const Mixin2 = function(superClass) {
return class extends superClass {
get reverse() {
this.name = [...this.name].reverse().join('')
return this
}
}
}
let mixins = [Mixin, Mixin2]
let MixedClass = mixins.reduce((base, mix) => mix(base), BaseClass)
let instance = new MixedClass('mark')
// use the new methods
console.log("reversed & uppercase:", instance.uppercase.reverse.name)
I have an app that initializes by running its method .init(params) like this:
app.init([TopBar, StatusBar, MainArea]);
Where TopBar, StatusBar and MainArea are classes, not instances of classes. Each of these classes implements the same interface IComponent.
I want to instantiate objects from the passed classes in the .init(params) method, like this:
init(params: IComponent[]): void {
params.map(function (component) {
let comp = new component();
this.components[comp.constructor.name] = comp;
}, this);
The issue is that as these are not instance, TypeScript doesn't know their types and throws an error:
error TS2345: Argument of type '(typeof TopBar | typeof StatusBar |
typeof MainArea)[]' is not assignable to parameter of type
'IComponent[]'.
How do I fix the code so that I could pass an array of classes that implement some interface to a method?
Typescript supports Class Type Generics (TypeScript Docs). Their example is:
function create<T>(c: {new(): T; }): T {
return new c();
}
Which says "Pass into my create method a class that when constructed will return the type T that I want". This signature will prevent you from trying to pass in any class type that isn't of type T.
This is close to what we want, we just need to adjust for it being an array of items and items of your IComponent.
public init(components: {new(): IComponent;}[]): void {
// at this point our `components` variable is a collection of
// classes that implement IComponent
// for example, we can just new up the first one;
var firstComponent = new components[0]();
}, this);
With the method signature, we can now use it like
app.init([TopBar, StatusBar, MainArea]);
Where we pass in the array of types that implement IComponent
There is a working typescript playground (run it to get alert with result)
what we need is to create a custom type InterfaceComponent. That will be expected as an array of the init() method
interface IComponent { }
class TopBar implements IComponent { }
class StatusBar implements IComponent { }
class MainArea implements IComponent { }
// this is a type we want to be passed into INIT as an array
type InterfaceComponent = (typeof TopBar | typeof StatusBar | typeof MainArea);
class MyClass {
components: {[key:string] : IComponent } = {};
init(params: (InterfaceComponent)[]): void {
params.map((component) => {
let comp = new component();
this.components[comp.constructor["name"]] = comp;
}, this);
}
}
let x = new MyClass();
x.init([TopBar, StatusBar, MainArea])
alert(JSON.stringify(x.components))
Check it here
Even though this is an old question: this is how you do it:
interface IComponent { something(): void; }
class TopBar implements IComponent { something() { console.log('in TopBar'); }}
class StatusBar implements IComponent { something() { console.log('in StatusBar'); }}
class MainArea implements IComponent { something() { console.log('in MainArea'); }}
interface ComponentClass {
new(): IComponent;
}
const components: { [name: string]: IComponent } = {};
function init(params: ComponentClass[]) {
params.map((component) => {
let comp = new component();
components[component.name] = comp;
});
}
init([TopBar, StatusBar, MainArea]);
for (const c in components) {
console.log('Component: ' + c);
components[c].something();
}
Use a factory method instead. The declaration is a bit clumsy but the idea works:
interface InterfaceComponent {
name: string;
}
class TopBar implements InterfaceComponent {
name: string;
}
class StatusBar implements InterfaceComponent {
name: string;
}
class MainArea implements InterfaceComponent {
name: string;
}
interface InterfaceComponentFactory {
create: () => InterfaceComponent;
}
function init(params: InterfaceComponentFactory[]): void {
params.map(function (component) {
let comp = component.create();
this.components[comp.name] = comp;
}, this);
}
init([{ create: () => new TopBar() }, { create: () => new StatusBar() }, { create: () => new MainArea() }]);
Perhaps you could specify the type of comp as InterfaceComponent.
var comp: InterfaceComponent = new component();
this.components[comp.constructor.name] = comp;
I found two different ways you can create types for this situation:
// Interface style:
export default interface IConstructor<T> extends Function {
new (...args: any[]): T;
}
// Union Type style:
export type ConstructorUnion<T> = new(...args : any[]) => T;
So this is how it would look with the IConstructor type:
interface IComponent { }
class TopBar implements IComponent { }
class StatusBar implements IComponent { }
class MainArea { }
class App {
public components: { [key: string]: IComponent } = {};
public init(params: IConstructor<IComponent>[]): void {
params.forEach((Component: IConstructor<IComponent>) => {
const comp: IComponent = new Component();
this.components[comp.constructor.name] = comp;
});
}
}
const app = new App();
app.init([TopBar, StatusBar, MainArea]);
console.clear();
console.log(app);
Here is the code:
https://stackblitz.com/edit/how-to-type-an-array-with-classes-in-typescript?file=index.ts