i am trying to build a class that build some dynamic methods on constructor stage, everything works well, but VS Code auto suggestion does not work for dynamic methods? how should we do that?
here is the codeSandBox
i have also tried interface but still no luck
export default class A {
private version = 1;
get getVersion() {
return this.version;
}
private actions = ["approve", "edit", "delete"];
constructor() {
this.actions.forEach(
method =>
(A.prototype[method] = route => {
console.warn(method + " called");
})
);
}
}
const B = new A();
console.warn(B.getVersion);
console.warn(B.approve()) // auto suggestion not available here
You can do this... but it is really rather hacky. Meta programming like this is impossible to fully type check. Here's a playground with this solution.
First, tell TS that actions is a constant array so that it can narrow the type.
private actions = ["approve", "edit", "delete"] as const;
Next, create a mapped type that describes what your forEach method adds to the type.
type _A = {
[K in A['actions'][number]]: (route: string) => void
}
Note that this has to be a type. It can't be an interface.
Finally, add an interface that extends this type alias. I expected TS to yell at me here about circularity, but apparently this is OK.
export default interface A extends _A {
}
Related
I am implementing a class like the following:
import * as User from './user';
export class Database {
constructor() {
for (const method in User) {
this[method] = User[method];
}
}
}
Where the ./user file contains:
export async function findById(id: number): Promise<User | null> {
return //
}
export async function findByName(name: string): Promise<User | null> {
return //
}
I can now use the Database class to perform operations, however there are no TypeScript hints since these are lost by dynamically reassigning the methods. I also have to include a #ts-nocheck in the Database file as I am otherwise getting the error:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'typeof import
Implementing a predefined interface also will not work due to it not registering the dynamic types (which is logical as these are loaded in at runtime). How do I implement a dynamic interface for this?
Big thanks
With suggestions from Keith I changed it to:
import * as User from './user';
export const Database = class _Database {
constructor() {
Object.assign(this, User);
}
} as { new (): typeof User };
This solved the TypeScript errors (yay)
This is something that if I'm able to achieve will be able to design a very easily extendible decorator factory for a project I'm working on.
Basically, I want to be able to use a super class's methods as a decorator for the sub-classes property.
This is usually how decorators work:
import { handleSavePropertyNameDecorator } from "./W-ODecorator"
class Main {
#handleSavePropertyNameDecorator
test1: string = ""
}
export default Main
And then the decorator function:
const propertyNames = [];
export const handleSavePropertyNameDecorator = (_instance: any, propertyName: string) => {
console.log("Executing handleSavePropertyNameDecorator")
propertyNames.push(propertyName);
}
However, instead of defining a separate function for the decorator, I'd like the decorator function to come from the super class:
import SuperClass from "./SuperClass"
class Main extends SuperClass{
#this.handleDecoratorFunction
test1: string = ""
}
export default Main
class SuperClass {
static propertyNameArray: string[] = [];
protected handleDecoratorFunction(_instance: any, propertyName: string) {
console.log("executing handle decorator function from super class!")
SuperClass.propertyNameArray.push(propertyName);
}
}
export default SuperClass;
Currently the keyword "this" has a compilation error that states it's possibly undefined. I've run this code and it doesn't execute the decorator function.
My question is this approach possible with some type of workaround? If this is possible that will significantly improve my organization.
Thank you in advance!
No, you can't do that, the decorator is applied when the class is defined not when it's instantiated. There are no instance methods from your superclass available.
However, you can do this with a static method of course:
class SuperClass {
static propertyNameArray: string[] = [];
protected static handleDecoratorFunction(_instance: any, propertyName: string) {
console.log("executing handle decorator function from super class!")
this.propertyNameArray.push(propertyName);
}
}
class Main extends SuperClass{
#SuperClass.handleDecoratorFunction
test1: string = ""
}
export default Main
I have a list of Components in a class Entity. These components extend the interface Component.
class Entity {
...
const components: Component[] = [];
...
}
Where specific components implements the interface Component
class SpecificComponent0 implements Component { ... }
Now I want to query the entity instance e and get a component if it matches the type fed into the query, something like this:
const specificComponent0 = e.getSpecificComponent<SpecificComponentClass0>();
Or perhaps like this
const specificComponent0 = e.getSpecificComponent(instanceof SpecificComponentClass0)
But I can't seem to figure out a way to do it in the entity's get function.
This is a tricky one as you are mixing runtime and build-time concerns. Referring to the examples you suggested:
const specificComponent0 = e.getSpecificComponent<SpecificComponentClass0>();
This definitely isn't going to work, because the angle brackets specify a "Type Parameter", which only exists at build time. Since what you are trying to do involves logic, you need to pass something into the function at runtime to help it pick the correct element.
const specificComponent0 = e.getSpecificComponent(instanceof SpecificComponentClass0)
The return value of the instanceof operator is a boolean value. You are passing either true or false into this function, which isn't very useful.
You have two problems here.
You want to pass something into the function that will allow you to select the right component
I am assuming you want the function to return a component narrowed to the correct type, rather than typed generically as Component
Problem 1 can be solved by passing in the type Constructor function and then matching it with the constructor property of the instantiated Component
class Entity {
constructor(private components: Component[]) {}
getSpecificComponent(thing: new () => Component): Component | undefined {
return this.components.find(component => component.constructor === thing)
}
}
This works perfectly fine, but your getSpecificComponent function is going to return a value typed as Component | undefined, which isn't very useful if you want to use properties that only exist on one of the specific types.
To solve Problem 2 (without casting the return value, which you really shouldn't do), we need to
Make the function generic and
Turn the predicate that is passed into find into a user defined type guard to give the compiler a hint that if that function returns true, it can safely narrow the type down to the generic type
class Component {}
class OtherThing1 extends Component { name = 'thing1' }
class OtherThing2 extends Component { name = 'thing2' }
class OtherThing3 extends Component { name = 'thing3' }
const getNarrower = <T extends Component>(thingConstructor: new () => T) =>
(thing: Component): thing is T => thing.constructor === thingConstructor
class Entity {
constructor(private components: Component[]) {}
getSpecificComponent<T extends Component>(thing: new () => T): T | undefined {
return this.components.find(getNarrower(thing))
}
}
const e = new Entity([new OtherThing1(), new OtherThing2()])
const thing = e.getSpecificComponent(OtherThing1)
console.log(thing) // [LOG]: OtherThing1: { "name": "thing1" }
const thingNotHere = e.getSpecificComponent(OtherThing3)
console.log(thingNotHere) // [LOG]: undefined
I get a circular dependency on my decorators because my class ThingA has a relation with ThingB and vice versa.
I've read several questions about this issue:
Beautiful fix for circular dependecies problem in Javascript / Typescript
TypeScript Decorators and Circular Dependencies
But I wasn't able to find a valid solution for my case.
I tried as many people suggest to change from #hasOne(ThingA) to #hasOne(() => ThingA) to force a lazy loading and break the dependency, but this solution doesn't work because I'm not able to get the constructor name.
I need the name of the constructor (example: 'ThingA') to add it in metadata of constructor ThingB.
Following my original code (without lazyload modification)
ThingA
#hasAtLeast(0, ThingB)
export class ThingA extends RTContent {
#IsEmail()
email: string;
}
ThingB
#hasOne(ThingA)
export class ThingB extends RTContent {
#IsString()
description: string;
}
Decorators:
export type LinkConstraint<C extends RTContent> = {
content: string; // name of the constructor (ex. 'ThingA')
maxOccurrences: number;
minOccurrences: number;
constructor: { new(...args: any[]): C };
}
function constraintFactory<C extends RTContent>(minOccurrences: number, maxOccurrences: number, associated: { new(...args: any[]): C }) {
return (constructor: Function) => {
const constraints = Reflect.getMetadata('linkConstraints', constructor) || [];
const constraint: LinkConstraint<C> = {
content: associated?.name,
minOccurrences,
maxOccurrences,
constructor: associated
};
constraints.push(constraint);
Reflect.defineMetadata('linkConstraints', constraints, constructor)
}
}
export function hasOne<C extends RTContent>(associated: { new(...args: any[]): C }) {
return constraintFactory(1, 1, associated)
}
export function hasAtLeast<C extends RTContent>(minOccurrences: number, associated: { new(...args: any[]): C }) {
return constraintFactory(minOccurrences, Infinity, associated)
}
I see that your decorator doesn't actually modify the constructor, it merely runs some side-effect code to add some metadata entry. Thus the #decorator syntax isn't a must.
My advice is that you decorate neither ThingA nor ThingB, just export them as-is. You defer the decoration in another module, which should be the common parent of both ThingA and ThingB. This way circular dependency is resolved.
For example, in './things/index.ts' you do:
import { ThingA } from './things/ThingA';
import { ThingB } from './things/ThingB';
hasOne(ThingA)(ThingB);
hasAtLeast(0, ThingB)(ThingA);
export { ThingA, ThingB }
Now other part of your code can import from './things/index.ts', instead of directly from './things/ThingA(or B).ts'. This would ensure the decoration is executed before instantiation of classes.
If you must use decorator, well lazy load is your best bet. #hasOne(() => ThingA) should does the trick, but you need to modify the implementation of hasOne accordingly, a little hack.
function hasOne(target) {
if (typeof target === "function") {
setTimeout(() => {
_workOnTarget(target())
}, 0)
} else {
_workOnTarget(target)
}
}
The key is to delay accessing variable value.
For this hack to work, we still rely on the fact that these decorators are side-effect only, don’t modify the constructor. So this is NOT a general solution to circular dependency problem. More general pattern is off course lazy evaluation. More complicated though, if you really need, pls ask in comments.
For your case, above impl should work. But you must not instantiate ThingA or B right inside any module’s top level, cuz that would happen before setTimeout callback, thus break the hack.
I'm running into a problem writing Typescript definitions for a third-party script that's using React PropTypes's instanceof check. Specifically React TimeSeries Charts and PondJS. (They don't have any created yet, which is why I'm under-taking it).
I've defined a Type file for Pond that looks like this
declare module 'pondjs' {
type Timestamp = number;
/* other type aliases */
export class TimeSeries {
constructor(
options: {
[index: string]: any;
}
);
static is: (arg1: TimeSeries, arg2: TimeSeries) => boolean;
range: () => any;
min: (key: string) => number;
max: (key: string) => number;
name: () => any;
atTime: (timestamp: Timestamp) => any;
/* Other methods */
}
/* Other Class Definitions */
}
This is then used by my wrapper class something like this
import { TimeSeries } from 'pondjs';
import { ChartContainer, ChartRow, LineChart } from 'react-timeseries-charts';
const timeSeries: TimeSeries = new TimeSeries({events: [{value: 15}]});
class myWrapper extends React.Component<Props, {}> {
render() {
return (
<ChartContainer>
<ChartRow>
<LineChart series={timeSeries} />
</ChartRow>
</ChartContainer>
);
}
This is an extremely reduced example, but my real code all works correctly, draws the correct thing to the page, and Typescript fails to compile if I pass incorrect parameters.
However, I'm getting a bunch of errors from PropTypes thrown from PropType's createInstanceTypeChecker.
"Warning: Failed prop type: Invalid prop `series` of type `TimeSeries` supplied to `LineChart`, expected instance of `TimeSeries`."
The problem is that LineChart checks series: propTypes.instanceof(TimeSeries), but it's of course internally importing Pond and checking against the "real" TimeSeries, not the one that has come through my Typescript type module definition.
When I step inside createInstanceTypeChecker to see what's going on, I can see that even though they're technically not the same class, (so I guess PropTypes is justified in its error) they are the same for all practical purposes, which is what I would expect Typescript to have produced.
> expectedClass === props[propName].constructor
false
> props[propName] instanceof expectedClass
false
> props[propName].constructor
ƒ TimeSeries(arg) {
(0, _classCallCheck3.default)(this, TimeSeries);
this._collection = null;
// Collection
this._data = null;
// Meta data
if (arg ins…
> expectedClass
ƒ TimeSeries(arg) {
(0, _classCallCheck3.default)(this, TimeSeries);
this._collection = null;
// Collection
this._data = null;
// Meta data
if (arg ins…
However, I would still expect that instanceof check to be true if Typescript is doing what I thought it did, something like the following
import { TimeSeries as _ts } from 'pondjs';
export class TimeSeries extends _ts {
/* same class, but with type extensions */
}
I suppose React TimeSeries Charts could "solve" by checking the shape of the prop instead of the specific constructor used. However, this must also be a very common issue when adding type definitions for third party libs, right? Since I can't find similar bug reports anywhere, I suspect I must be missing something obvious.
I don't want to disable PropTypes completely. I'd either like to find out I'm misconfiguring this type declaration file and change it so PropTypes checking instanceof works, or be able to update React TimeSeries Charts with a different equally simple check that PropTypes could do to convince itself the Typescript-enhanced classes are still essentially the same class, without resorting to comparing the shape key by key.
Versions:
Typescript 2.5.0
React 15.6.1
Proptypes 15.5.10
PondJS 0.8.7
React TimeSeries Charts 0.12.8