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);
Related
I have the following setup, where I want to use TypeScript to make sure the user is only choosing types that are allowed in the interface.
This works fine if I call the class directly, but if I use a custom generic function to pass back same class function it does not work and gives an error.
Can anyone help me understand how to fix this?
export interface IMyInterface {
test1: {
test1Sub: {
test1SubSub: string
}
},
test2: {
test2Sub: {
test2SubSub: string
}
}
}
export default class MyClass<T> {
parentFunction = <A extends keyof T>(app: A) => (
{
childFunction: (page: keyof T[A]) => {
console.log(app, page)
}
}
)
}
// simplified for example
export function useMyCustomHook<T> (app: keyof T) {
const settings = new MyClass<T>();
return settings.parentFunction(app)
}
// Works when calling class directly
const myClass = new MyClass<IMyInterface>()
const parent = myClass.parentFunction('test1')
const child = parent.childFunction('test1Sub')
// does not work when calling via hook
const myHookValue = useMyCustomHook<IMyInterface>('test1')
myHookValue.childFunction('test1') // ERROR - Argument of type 'string' is not assignable to parameter of type 'never'.
I have created a playground example of this.
TypeScript Playground
You just need an explicit type for the app parameter:
export function useMyCustomHook<T, A extends keyof T>(app: A) {
const settings = new MyClass<T>();
return settings.parentFunction(app);
}
const myHookValue = useMyCustomHook<IMyInterface, "test2">("test2");
myHookValue.childFunction('test2Sub')
This is because without extending keyof T, inside the scope of useCustomHook the parameter app will always be treated as the widest type keyof T. When you use it with childFunction that creates a function signature of:
childFunction: (page: keyof T[keyof T]) => {
console.log(app, page)
}
The type keyof T[keyof T] on MyInterface resolves to an intersection of two string literal types "test1Sub" and "test2Sub", which is always never.
I'd like to use decorators in my project. Here's what I have written:
export const DetachedWindow = (options: TDetachedWindowOptions = defaultOptions) => <R extends Constructable>(Clazz: R): R => {
const tmp = class extends Clazz {
value = 'value';
value2 = 'value2';
constructor(...args: any[]) {
super(...args);
// <some logick>
}
};
Object.defineProperty(tmp, 'name', { value: Clazz.name });
return tmp;
};
As you see this decorators creates a few fields. But typescript can't recognize them
#DetachedWindow({ optA: 'optA' })
export class SomeClass {
constructor() {
setTimeout(() => {
console.log(this.value); // TS2339: Property 'value' does not exist on type 'SomeClass'.
});
}
}
It does exsist though. If I add #ts-ignore before using these parameters, the code works okay.
I wounder, how can I create a class decorator, that would extend parent's type. I tried to do something like this:
export const DetachedWindow = (options: TDetachedWindowOptions = defaultOptions) => <R extends Constructable>(Clazz: R): R & { value: string } => {
But it didn't help.
Any ideas?
The answer is in the TypeScript docs of the Class decorators:
// Note that the decorator _does not_ change the TypeScript type
// and so the new property `reportingURL` is not known
// to the type system:
bug.reportingURL;
Property 'reportingURL' does not exist on type 'BugReport'.
I need help using class mixins in declaration files. Specifically, when a method is defined in a mixin, typescript is not picking it up in the mixed class body:
In my case, I am applying two mixins. The first mixin - NotifyingElementMixin - provides a method called notify, and it's this method which is failing to apply to the mixed class body
notifying-element-mixin.js
export const NotifyingElementMixin = superclass =>
class NotifyingElement extends superclass {
/**
* Fires a `*-changed` event.
*
* #param {string} propName Name of the property.
* #param {any} value property value
* #protected
*/
notify(propName, value) {
this.dispatchEvent(
new CustomEvent(`${propName}-changed`, {
detail: { value },
})
);
}
};
};
notifying-element-mixin.d.ts
export declare class NotifyingElement {
public notify(propName: string, value: any): void
}
export function NotifyingElementMixin<TBase extends typeof HTMLElement>
(superclass: TBase): TBase & NotifyingElement;
The second mixin provides other properties and methods, but for the sake of this question, I've simplified the implementation
apollo-query-mixin.js
export const ApolloQueryMixin =
superclass => class extends superclass {
data = null;
is = 'Query';
};
apollo-query-mixin.d.ts
export declare class ApolloQuery<TCacheShape, TData, TVariables, TSubscriptionData = TData> {
data: null
is: string
}
type Constructor<T = HTMLElement> = new (...args: any[]) => T;
export function ApolloQueryMixin<TBase extends Constructor, TCacheShape, TData, TVariables>
(superclass: TBase): ApolloQuery<TCacheShape, TData, TVariables> & TBase;
Finally, I want to export a class which applies both mixins and provides it's own methods as well. This is where I run into trouble
apollo-query.js
class ApolloQuery extends NotifyingElementMixin(ApolloQueryMixin(HTMLElement)) {
/**
* Latest data.
*/
get data() {
return this.__data;
}
set data(value) {
this.__data = value;
this.notify('data', value);
}
// etc
}
apollo-query.d.ts
import { ApolloQueryMixin } from "./apollo-query-mixin";
import { NotifyingElementMixin } from "./notifying-element-mixin";
export declare class ApolloQuery<TBase, TCacheShape, TData, TVariables>
extends NotifyingElementMixin(ApolloQueryMixin(HTMLElement)) {}
When I compile this, or use my IDE, I receive the error:
error TS2339: Property 'notify' does not exist on type 'ApolloQuery'.
How do I finagle typescript into picking up my inherited methods in the mixed class body?
Here's the mixin pattern I use, I think the key is the return constructor:
import { LitElement, property } from "lit-element";
type Constructor = new (...args: any[]) => LitElement;
interface BeforeRenderMixin {
beforeRenderComplete: Boolean;
}
type ReturnConstructor = new (...args: any[]) => LitElement & BeforeRenderMixin;
export default function<B extends Constructor>(Base: B): B & ReturnConstructor {
class Mixin extends Base implements BeforeRenderMixin {
#property({ type: Boolean })
public beforeRenderComplete: boolean = false;
public connectedCallback() {
super.connectedCallback();
if (!this.beforeRenderComplete)
this.beforeRender().then(() => (this.beforeRenderComplete = true));
}
public async beforeRender() {
return;
}
public shouldUpdate(changedProperties: any) {
return this.beforeRenderComplete && super.shouldUpdate(changedProperties);
}
}
return Mixin;
}
which generates:
import { LitElement } from "lit-element";
declare type Constructor = new (...args: any[]) => LitElement;
interface BeforeRenderMixin {
beforeRenderComplete: Boolean;
}
declare type ReturnConstructor = new (...args: any[]) => LitElement & BeforeRenderMixin;
export default function <B extends Constructor>(Base: B): B & ReturnConstructor;
export {};
I am using typescript and i am writing a custom decorator for one of my angular class. I want to access the base class method in the child class decorator. Or access base class methods using the child class prototype. Is there any way to do this? Problem explained in detail below.
Scenario
I have a base class which is like
export class Base {
public init() {
console.log('My base class function');
}
}
And i have a derived class which extends this base class
export class Child extends Base {
}
What i am trying to do
I am trying to write a decorator for the derived class something like
#TestDecorator(['init'])
export class Child extends Base {
}
which will call the init method from the base class.
What is the issue
To do the above scenario, i have written code something like below
export function Tool<T extends Base>(methods: any[]) {
return function (target: Function) {
methods.forEach((item) => {
if (item === 'init') {
target.super.init() // Stuck here
}
})
}
}
I am not understanding how to make the following line work
target.super.init() // Stuck here
Please help me with the solution. I am stuck. Thanks
I believe you are looking for something like this:
export function Tool<T extends Base>(methods: any[]) {
return function (target: Function) {
return class extends target {
constructor(...args: any[]) {
super(...args)
methods.forEach((item) => {
if (item === 'init') {
super.init( );
}
})
}
}
}
}
To expand on Paulpro's answer, since the decorator function is returning a substitute for the constructor of the class that it is decorating, it must maintain the original prototype.
In the following example, there is an error due to the missing init() method in TestDecorator<Base>.
Typescript Playground Demo
class Base {
public init() {
console.log('My base class function');
}
}
function TestDecorator<T extends Base>(methods: any[]) {
return function (target: any) {
return class extends target {
constructor(...args: any[]) {
super(...args)
methods.forEach((item) => {
if (item === 'init') {
super.init( );
}
})
}
}
}
}
#TestDecorator(['init']) // Error: Property 'init' is missing in type 'TestDecorator<Base>.(Anonymous class)' but required in type 'Child'.
class Child extends Base {
}
let c = new Child();
Corrected Decorator
function TestDecorator<T extends Base>(methods: any[]) {
return function (target: any) {
return class extends target {
init() {} // Define init()
constructor(...args: any[]) {
super(...args)
methods.forEach((item) => {
if (item === 'init') {
super.init( );
}
})
}
}
}
}
Class Decorators
If the class decorator returns a value, it will replace the class declaration with the provided constructor function.
NOTE: Should you choose to return a new constructor function, you must take care to maintain the original prototype. The logic that applies decorators at runtime will not do this for you.
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