TypeScript generics create instace - javascript

I'm trying to create a factory for instantiating my classes with generics. Checked out TypeScript docs and it all works perfectly. In short, this works just fine:
class Person {
firstName = 'John';
lastName = 'Doe';
}
class Factory {
create<T>(type: (new () => T)): T {
return new type();
}
}
let factory = new Factory();
let person = factory.create(Person);
console.log(JSON.stringify(person));
Now define class Person in directory:
export class Person extends BasePerson {
firstName = 'John';
lastName = 'Doe';
}
And when I import Person from other package:
import { Person } from "./directory"
class Factory {
create<T>(type: (new () => T)): T {
return new type();
}
}
let factory = new Factory();
let person = factory.create(Person);
I get error:
Argument of type 'typeof Person' is not assignable to parameter of type 'new () => Person'
How can I get a value of Person instead of typeof Person?
Using TypeScript 3.7.2 and Node v10.13.0.

Could you try this for me please?
import { Person } from "./directory"
class Factory {
create<T>(type: (new () => T)): T {
return new type();
}
}
let factory = new Factory();
let person = factory.create(new Person);

Actual problem here was a parent class of Person -> BasePerson. BasePerson expected an argument in its constructor, so when I tried to call factory.create(Person), Person actually was an typeof because it expected an argument for base class constructor. Problem was solved by deleting the constructor in base class and injecting a property via ioc container, in my case Inversify.

Related

typescript class to throw error when undefined is passed as a parameter to constructor

I have a class with a lot of parameters, a simplified version is shown below:
class data {
ID: string;
desp: string;
constructor(con_ID:string,con_desp:string){
this.ID = con_ID;
this.desp = con_desp;
}
}
I am then receiving data from a RESTful call, the body of the call is JSON. It might not have all the parameters requried to create an instance of data. Below is an example of the desp not being passed.
const a = JSON.stringify({ ID: 'bob' });
const b = JSON.parse(a)
If I try to create a new instance of data, it works.
console.log(new data(b['ID'], b['desp']))
>> data { ID: undefined, desp: 'bob' }
How do I reject the construction of the class if a parameter from JSON is undefined?
One method would be to do this for each parameter within the constructor, but I don't think this is the correct solution:
if (con_ID== undefined){
throw new Error('con_ID is undefined')
}
We can utilize class decorators for this. If we return a class from the decorator then the class' constructor will replace the one defined in code. Then we use parameter decorators to store the index of each parameter we wish to check into an array.
const noUndefinedKey = Symbol("noUndefinedKey");
const NoUndefined: ParameterDecorator = function (target, key, index) {
const data = Reflect.getMetadata(noUndefinedKey, target) ?? [];
data.push(index);
Reflect.defineMetadata(noUndefinedKey, data, target);
};
const Validate = function (target: { new (...args: any[]): any }) {
const data = Reflect.getMetadata(noUndefinedKey, target);
return class extends target {
constructor(...args: any[]) {
data.forEach((index: number) => {
if (typeof args[index] === "undefined") throw new TypeError(`Cannot be undefined.`);
});
super(...args);
}
}
}
Note that reflect-metadata must be used to use Reflect.getMetadata and Reflect.defineMetadata. Here's how you would use it:
#Validate
class ProtectedFromUndefined {
constructor(#NoUndefined param: string) {}
}
And try a few things:
//#ts-ignore throws error because undefined was provided
new ProtectedFromUndefined()
//#ts-ignore
new ProtectedFromUndefined(undefined)
// is ok
new ProtectedFromUndefined("")
Playground

Assign an imported function as a class method?

Is it possible to assign an imported function as a class method, so that it automatically goes on an objects prototype chain?
// Module
module.exports = function testMethod() {
console.log('From test method')
}
// Main
const testMethod = require('./testMethod')
class TestClass {
// Replace this method with imported function
testMethod() {
console.log('From test method')
}
}
const obj = new TestClass()
I was able to attach the method in this constructor using this.testMethod = testMethod but the method did not go on an objects prototype chain.
Assign to the .prototype property of the TestClass so that instances of TestClass will see the imported method:
class TestClass {
}
TestClass.prototype.testMethod = testMethod;
const testMethod = () => console.log('test method');
class TestClass {
}
TestClass.prototype.testMethod = testMethod;
const tc = new TestClass();
tc.testMethod();

use plainToClass in constructor

I have a constructor that assigns properties to the instance:
class BaseModel {
constructor (args = {}) {
for (let key in args) {
this[key] = args[key]
}
}
}
class User extends BaseModel {
name: string
}
Then I can create an instance like this:
let user = new User({name: 'Jon'})
I would now like to replace this basic functionality with class-transformer:
let user = plainToClass(User, {name: 'Jon'})
My codebase uses the first approach in many places and I would therefore like to implement the new way in the constructor so I don't break the old code:
constructor (args = {}) {
let instance = plainToClass(CLASSTYPE, args)
for (let key in Object.keys(instance)) {
this[key] = instance [key]
}
}
How can I get the class type in the constructor? I cannot use User because this is the base model and other classes may also extend from it.
I suppose it's new.target. Doc
To get constructor use new.target.prototype.constructor, and it's property name - to get class name.
class BaseModel {
typeName: string;
constructor (args = {}) {
this.typeName = new.taget.constructor.name;
}
}
class User extends BaseModel {
}
const user = new User();
const typeName = user.typeName; // this would return 'User'

Is it possible to type properties added by a decorator?

Let's say I have a decorator like in the following
example
which adds a new name property with a preset value.
Is there any way to tell typescript that my decorator adds this property so that all decorated classes are typed correctly?
Example code:
function withName(name) {
return (target) => {
target.prototype.name = name;
}
}
#withName('Karl')
class Person {
greet() {
console.log("Hi I'm ", this.name);
}
}
const karl = new Person();
karl.greet(); // Will log: "Hi I'm Karl"
console.log(karl.name); // <- Will cause a TypeScript error: "Property 'name' does not exist on type 'Person'"
Decorators by design are not allowed to change the structure of the type they are decorating. A supported way of doing this would be to use a mixin, as described here
function withName(nameValue: string) {
return <T extends new (...args: any[]) => any>(target: T) => {
return class extends target {
name: string = nameValue
}
}
}
const Person = withName("Karl")(class {
greet() {
console.log("Hi I'm ...");
}
});
const karl = new Person();
console.log(karl.name);
The approach above only augments the class to the outside world so the added members are not visible from inside the class. We could augment the base class of Person (or use an empty class if there is no base class) to get access to the added fields inside the augmented class:
class Person extends withName("Karl")(class { }) {
greet() {
console.log("Hi I'm ", this.name);
}
}
const karl = new Person();
karl.greet(); // Will log: "Hi I'm Karl"

JS immutable objects keep the same object type

Let's say I have a class.
class Test {
constructor() {
this.name = 'name';
this.arr = [];
}
}
And I need to create multiple instances from this class.
const entities = {
1: new Test(),
2: new Test()
}
Now, I need to update one of the properties in a shallow clone manner.
const newEntities = {
...entities,
[1]: {
...entities[1],
name: 'changed'
}
}
console.log(newEntities[1].arr === entities[1].arr) <=== true
That's works, but the problem is that [1] is a simple object and not instance of Test anymore.
How can I fix that?
You can't keep instances using object destructuring so you'll need implement this behaviour.
First example, set the new properties in the constructor:
class Test {
constructor(props) {
props = props == null ? {} : props;
this.name = 'name';
this.arr = [];
Object.assign(this, props);
}
}
const newEntities = {
...entities,
[1]: new Test({ ...entities[1], name: 'changed' })
}
Sencod example, use a custom method:
class Test {
constructor() {
this.name = 'name';
this.arr = [];
}
assign(props) {
props = props == null ? {} : props;
const instance = new Test();
Object.assign(instance, this, props);
return instance;
}
}
const newEntities = {
...entities,
[1]: entities[1].assign({ name: 'changed' })
}
You can use Object.setPrototypeOf on your [1] object.
As result, it will be:
Object.setPrototypeOf(newEntities[1], Test.prototype);

Categories

Resources