TypeScript: auto-generated dynamic function names - javascript

I have some dynamically generated function names in TypeScript. The only way I can use them now is to cast my objects to <any>. Ex.: <any>myInstance.getDataA(). These functions are dynamically generated based on some rules. Based on the same rules I'd like to generate type-definitions for my classes, but I can not make it work.
original.ts
abstract class Original {
dynamics = ['getData', 'setData'];
constructor() {
// I create functions here dynamically
this.dynamics.forEach((key) => {
this[key + this.info] = () => null;
});
}
get info() {
return 'X';
}
}
my-class.ts
class MyClass extends Original {
get info() {
return 'A';
}
}
my-other-class.ts
class MyOtherClass extends Original {
get info() {
return 'B';
}
}
something.ts
const myInstance = new MyClass();
console.log(myInstance.getDataA()); // TS2339: Property getDataA does not exist on type: 'MyClass'
const myOtherInstance = new MyOtherClass();
console.log(myInstance.getDataB()); // TS2339: Property getDataB does not exist on type: 'MyClass'
I would like to automatically generate a definition file to define these dynamic properties.
Ex.:
my-class.def.ts
declare interface MyClass {
getDataA;
setDataA
}
//my-other-class.def.ts
declare interface MyClass {
getDataB;
setDataB
}
But I can not find a syntax for my definition files to make it work. Pls ask me if I was not clear, and pls help if you have any idea!

Edit for 4.1
Using Template literal types and mapped type 'as' clauses we can now do concatenate strings in the type system and create a class that has these properties created dynamically.
function defineDynamicClass<T extends string[]>(...info: T): {
new (): {
[K in T[number] as `get${Capitalize<K>}`]: () => unknown
} & {
[K in T[number] as `set${Capitalize<K>}`]: (value: unknown) => void
} & {
info: T
}
} {
return class {
get info () {
return info;
}
} as any
}
class MyClass extends defineDynamicClass('A', 'B', 'ABAB') {
}
let s =new MyClass();
s.getA();
s.getABAB();
s.setA("")
s.info;
Playground Link
Before 4.1
The within language approach
There is no way to do this within the type system, since we can't perform string manipulation on string literal types. The closest you can get, without external tools, is to create get/set methods that take a string literal type, that will be of the same as that returned by the getInfo method.
function stringLiteralArray<T extends string>(...v: T[]){ return v;}
abstract class Original {
get(name: this['info'][number]) {
return null;
}
set(name: this['info'][number], value: any) {
return null;
}
get info() : string[]{
return [];
}
}
class MyOtherClass extends Original {
get info() {
return stringLiteralArray('A', 'B', 'ABAB');
}
}
class MyClass extends Original {
get info() {
return stringLiteralArray('C', 'D', 'DEDE');
}
}
let s =new MyClass();
s.get('A') // error
s.get('C') // ok
While this approach is not 100% what you want, form our previous discussions the aim was to have full code-completion for the methods, and this approach achieves this. You get errors if you pass in the wrong value and you get a completion list for the string:
The compiler API approach
A second approach would be to create a custom tool that uses the typescript compiler API to parse the ts files, look for classes derived from Original and generates interfaces containing the methods (either in the same file or a different file) , if you are interested in this I can write the code, but it's not trivial, and while the compiler API is stable I don't think the compiler team takes as much care with backward compatibility as they do with the language (in fact this is the exact statement they make in the documentation page).
If you are interested in such a solution, let me know and I can provide it, but I advise against it.

Related

how forcing a type instance instead of typeof in typescript

how can I export a type by forcing it to be an instance.
I have tried many ways, I only found one solution, creating a static getter but I would like to remove my static getter.
Here context:
I would like to export a type of a instance of A from there $A.A, for ref only.
export const $A = (() => {
class A {
static get default() {
return A.create();
}
static create() {
return new A();
}
constructor() {}
}
return { A };
})();
i try many way, here 7 of them ! no one work instead the way 1 ! but it because i add a static getter in the js class.
export type _1 = typeof $A.A.default;
export type _2 = typeof new $A.A;
export type _3 = typeof $A.A.create();
export type _4 = typeof $A.A();
export type _5 = typeof $A['A'];
export type _6 = $A.A;
export type _7 = typeof new ()=>$A.A;
// example somewhere in the project, i want tell A should be a instance and not a typeof!
function foo(A:_6)
So what the syntax to emulate a instance in a ts type for export somewhere for typage usage only.
My project is in js, but using ts only for help the tsserver to understand my refs when he dont.
So it for Intelisence in my ide only and no for generate ts=>js.
Preliminary note: the class A code here lacks any instance structure (no properties or methods). All non-nullish values will be assignable to that instance type; see this FAQ entry for more info. Just to avoid this weirdness, I've added a property to the example class:
const $A = (() => {
class A {
static get default() {
return A.create();
}
static create() {
return new A();
}
constructor() { }
someStructure = 123; // add structure here
}
return { A };
})();
Now the compiler can tell that {someRandomThing: 123} is not compatible with the A type you're having trouble naming.
You might want to use the InstanceType<T> utility type to pull out the return type of a construct signature:
type A = InstanceType<typeof $A.A>
You could write this yourself using conditional type inference:
type AlsoA = typeof $A.A extends new (...args: any) => infer I ? I : never;
Or, you could use the method we had to use before conditional types existed: TypeScript pretends that the prototype property of a class is the same as its instance type. This isn't really true since the prototype generally only contains the methods and not other properties. But you can use it anyway:
type AlsoAlsoA = typeof $A.A.prototype;
Any of those should produce the same type.
Let's make sure it works:
function foo(a: A) { }
foo($A.A.create()) // okay
foo({ someRandomThing: 123 }) // error
// Argument of type '{ someRandomThing: number; }' is
// not assignable to parameter of type 'A'.
Looks good!
Playground link to code

How to make class instance constructable in TypeScript?

I'm trying to convert this package to TypeScript without any breaking changes. I have the following code in TypeScript.
// DocumentCarrier.ts
/* export */ class DocumentCarrier {
internalObject: {};
model: Model;
save: (this: DocumentCarrier) => void;
constructor(model: Model, object: {}) {
this.internalObject = object;
this.model = model;
}
}
DocumentCarrier.prototype.save = function(this: DocumentCarrier): void {
console.log(`Saved document ${JSON.stringify(this.model)} to ${this.model.myName}`);
};
// Model.ts
// import {DocumentCarrier} from "./DocumentCarrier.ts";
/* export */class Model {
myName: string;
Document: typeof DocumentCarrier;
get: (id: number) => void;
constructor(name: string) {
this.myName = name;
const self: Model = this;
class Document extends DocumentCarrier {
static Model: Model;
constructor(object: {}) {
super(self, object);
}
}
Document.Model = self;
Object.keys(Object.getPrototypeOf(this)).forEach((key) => {
Document[key] = this[key].bind(this);
});
this.Document = Document;
return this.Document as any;
}
}
Model.prototype.get = function(id: number): void {
console.log(`Retrieving item with id = ${id}`);
}
// Usage
// index.ts
// import {Model} from "./Model.ts";
const User = new Model("User");
const user = new User({"id": 5, "name": "Bob"});
user.save(); // "Saved document {"id": 5, "name": "Bob"} to User"
console.log(User.Model.myName); // "User"
// console.log(User.myName); // "User" // This option would be even better, but isn't supported in the existing code
User.get(5); // "Retrieving item with id = 5"
In the Usage section (very bottom of the code example above) I'm getting multiple errors in TypeScript. But running that code in a JavaScript file, works perfectly. So I know it's working and the code is accurate.
I think the biggest problem of what I'm trying to do is return this.Document as any. TypeScript is interpreting that as casting this.Document to a Model instance, when in reality it's not.
My question is this. In TypeScript how can I set it up where you can run new MyClassInstance() and have it return an instance of a different class? That has a bidirectional reference from MyClassInstance and the different class. In short, how do I get the following code working?
It's important that any solution works with the Usage section, and no modifications are made to that section. Except for the User.Model.myName vs User.myName section, which would be preferred as User.myName, but in the existing version functions as User.Model.myName.
For easy use, I also created a TypeScript Playground.
I'm going to interpret this question strictly as "how can I give typings to the existing code so that the compiler understands the code in the Usage section?" That is, the answer should not touch the emitted JavaScript, but instead should only alter type definitions, annotations, and assertions.
Aside: the more general question "how should I implement a class whose instances are themselves class constructors" is one I won't attempt to address, since from my research the best answer here is "don't try to do that" since it plays poorly with the prototypical inheritance model in JS. I'd instead lean strongly toward having a non-constructible class instance hold a property which is the constructor of the new class. Something like this Playground code. You'd be a lot happier in the long run, I expect.
Back to the typings: the main problem here is that TypeScript has no way to specify that a class constructor returns a type other than the class being defined. This is either intentional (see microsoft/TypeScript#11588 or a missing feature (see microsoft/TypeScript#27594) but in any case it's not part of the language.
What we can do here is to use declaration merging. When you write class Model {} you introduce both a class constructor object named Model and an interface type named Model. That interface can be merged into, adding methods and properties that the compiler doesn't already know about. In your case you could do this:
interface Model {
new(object: {}): DocumentCarrier;
Model: Model;
}
This lets the compiler know that Model instances, in addition to having the properties/methods declared in the class, also has a Model property whose type is Model, and, importantly, a constructor signature. That's enough to get the following code to compile without error:
const User = new Model("User");
const user = new User({ "id": 5, "name": "Bob" });
user.save(); // "Saved document {"id": 5, "name": "Bob"} to User"
console.log(User.Model.myName); // "User"
User.get(5); // "Retrieving item with id = 5"
The compiler does think that User.myName exists, which it doesn't at runtime, but that's already a problem with the existing code so I'm not touching that here. It's possible to change the typings further so that the compiler knows that User.Model.myName exists and that User.myName does not exist, but that becomes quite complicated as it requires you to split Model's interface into multiple types that you carefully assign to the right values. So for now I'm ignoring it.
The only other change I'd make here would be to give different typings to the implementation of Model, like this:
class Model {
myName: string;
Document: Model;
get!: (id: number) => void;
constructor(name: string) {
this.myName = name;
const self: Model = this;
class Document extends DocumentCarrier {
static Model: Model;
constructor(object: {}) {
super(self, object);
}
}
Document.Model = self;
(Object.keys(Object.getPrototypeOf(this)) as
Array<keyof typeof DocumentCarrier>).forEach((key) => {
Document[key] = this[key].bind(this);
});
this.Document = Document as Model;
return this.Document;
}
}
The only thing the compiler won't be able to verify in the above is that the Document class is a valid Model, so we use the assertion Document as Model. Other than that I just put a few assertions (get is definitely assigned, and Object.keys() will return an array of keys of the DocumentCarrier constructor) so that you don't need to turn off the --strict compiler flag.
Okay, hope that helps. Good luck!
Playground link to code
After roaming a bit, I got something.
Typescript complains about your solution because, even if you are returning a class Document internally in Model constructor, the compiler expects a Model instance, which is not constructable.
So, we need to make Model constructable. In fact, the same as making a function which returns instances of something.
First, let's declare your preovious DocumentCarrier class. Now, DocumentCarrier will have two properties, model and name (this was your previously keyed myName from Model class).
class DocumentCarrier {
name: string = ``;
constructor(public model: {}) { }
save = () => console.log(`Saved document ${JSON.stringify(this.model)} to ${this.name}`)
}
After that, we need that function declaration that returns an instance model of type DocumentCarrier.
const Model = (name: string) => {
return class extends DocumentCarrier {
name: string = name;
constructor(model: any) {
super(model);
}
}
}
The function takes a string parameter and returns a constructor of type DocumentCarrier which takes an any object on its constructor and passes to the DocumentCarrier constructor.
We can call Model like this.
const User = Model('User');
const user = new User({id: 5, name: 'Bob'});
user.save(); // Saved document {"id":5,"name":"Bob"} to User
The call to Model is the only change made. Now Model call does not need a new keyword.
On the other hand, name in DocumentCarrier can be accessed from the last instance constructed.
In addition, this solution could be a bit more generic by defining generic types.
type Constructor<T> = new (...args: any[]) => T;
The type Constructor constraints a constructor function of any type T.
Now, we need to rewrite the Model function:
const Model = <T extends Constructor<{}>>(Type: T, name: string) => {
return class extends Type {
name: string = name;
constructor(...args: any[]) {
super(...args);
}
}
}
Model now expects the same string as before but an additional parameter which is the type we want to instantiate, so
const User = Model(DocumentCarrier, 'User');
const user = new User({id: 5, name: 'Bob'});
user.save(); // Saved document {"id":5,"name":"Bob"} to User
Even more, since we are fulfilling a property name that belongs to the instance we are creating inside the Model function, we should constraint the input type to expect that name property.
type Constructor<T> = new (...args: any[]) => T;
interface DynamicallyConstructed {
name: string;
}
class DocumentCarrier implements DynamicallyConstructed {
name: string = ``;
constructor(public model: {}) { }
save = () => console.log(`Saved document ${JSON.stringify(this.model)} to ${this.name}`)
}
const Model = <T extends Constructor<DynamicallyConstructed>>(Type: T, name: string) => {
return class extends Type {
name: string = name;
constructor(...args: any[]) {
super(...args);
}
}
}
const User = Model(DocumentCarrier, 'User');
const user = new User({id: 5, name: 'Bob'});
user.save();
Hope this helps a bit.
Please comment any issue.

Declaring types for properties generated by a TypeScript class decorator?

Let's say I've declared the following class to use as a decorator in TypeScript:
class Property {
static register(...props: Property[]) {
return function(cls: any) {
props.forEach(prop => {
Object.defineProperty(cls.prototype, prop.propertyName, {
get() {
return this[`${prop.propertyName}_`] + ' (auto-generated)';
},
set(value: any) {
this[`${prop.propertyName}_`] = value;
},
});
});
};
}
constructor(private readonly propertyName: string) {}
}
When applied to a class like this:
#Property.register(new Property('myCustomProperty'))
class MyClass {}
The result is a class with an auto-generated property setter/getter named myCustomProperty:
const obj = new MyClass();
obj['myCustomProperty'] = 'asdf';
console.info(obj['myCustomProperty_']); // outputs 'asdf'
console.info(obj['myCustomProperty']); // outputs 'asdf (auto-generated)'
Unfortunately, TypeScript does not seem to have any knowledge about this property at compile time since it is being generated at runtime.
What is the best way to ensure TypeScript recognizes the existence of these auto-generated properties? Ideally everything would be auto generated for me so that I didn't have to maintain any extra types somewhere else.

With Typescript in Protractor, how can I scope an interior function so it's accessible to another function?

We are creating a library for frequently used functions in our Protractor/TypeScript project, and encountered a problem with scoping.
This is an excerpt from the TypeScript. Our problem occurs when we run the application and call for example clickBreadcrumb. The clickBreadcrumb function attempts to access the clickRepeaterElement.byName function.
export class Lib {
public clickBreadcrumb = {
byText(breadcrumbText: string) {
// This statement fails. TypeError: Cannot read property 'byName' of undefined
this.clickRepeaterElement.byName('bcItem in vm.breadCrumbs', breadcrumbText);
}
};
public clickRepeaterElement = {
byName(repeaterText:string, elementToClick:string, parentElement?:protractor.ElementFinder): void {
// Click the elementToClick.
},
byIndex(repeaterText: string, index: number) {
// Click element at the index.
},
};
}
WebStorm resolves clickRepeaterElement.byName, which essentially signals to us that this should work. But when we actually run the test, we get the following error:
TypeError: Cannot read property 'byName' of undefined
Coming from a C# background this was unexpected. How can we adjust the pattern so that this will resolve as we expect? Thanks for your help.
Javascript has weird rules when it comes to this.
In your case this points to the byText function, not the class.
I would rewrite it this way:
export class Lib {
public clickBreadcrumb = {
byText: this.byText
};
public clickRepeaterElement = {
byName: this.byName,
byIndex: this.byIndex,
};
private byText(breadcrumbText: string) {
// this is now Lib
this.clickRepeaterElement.byName('bcItem in vm.breadCrumbs', breadcrumbText);
}
private byName(repeaterText: string, elementToClick: string, parentElement ? : protractor.ElementFinder): void {
// Click the elementToClick.
}
private byIndex(repeaterText: string, index: number) {
// Click element at the index.
}
}
You can also use bind to make sure that the context of the methods have the correct value of this.
Update:
Regarding the multiple implementations question. I would propose you make use of the classes in TypeScript to structure the code a little bit differently.
export class Lib {
public clickBreadcrumb = new Breadcrumb(this);
public clickRepeaterElement = new Repeater(this);
}
export class Breadcrumb {
constructor(private lib: Lib) {}
public byText(breadcrumbText: string) {
this.lib.clickRepeaterElement.byName('bcItem in vm.breadCrumbs', breadcrumbText);
}
}
export class Repeater {
constructor(private lib: Lib) {}
public byName(repeaterText: string, elementToClick: string, parentElement ? : protractor.ElementFinder): void {
// Click the elementToClick.
}
public byIndex(repeaterText: string, index: number) {
// Click element at the index.
}
public byText(test: string) {
// some other implementation
}
}
You can also send smaller parts of the library in places, instead of sending Lib everywhere. It will also allow for better concern separation if/when your library grows.

How do I add a static method to my TypeScript class?

I am trying to define an API using TypeScript such that it can work like this:
// Create new user (Working)
var user : IUser = new Api.User({ firstName: "John", lastName: "Smith" });
// Delete existing user (Working)
Api.User.Delete(1);
// Load existing user (Unsure how to implement)
var user = Api.User(123);
My TypeScript:
module Api
{
export class User
{
constructor(id : number)
constructor(user : IUser)
constructor(user? : any)
{
// ...
}
static Delete(id : number) {}
}
}
I am not sure how to have a static method Api.User() i.e. not use new. I don't know what to call this type of construct, which makes it difficult to research. :(
I did try adding an unnamed static to the User class, but this isn't right.
static (id : number)
{
// ...
}
Option 1: export a function directly on the Api module
You can export a function on the Api module to retrieve/create a User instance:
module Api
{
export class User
{
}
export function GetUser(id: number):User {
return new User();
}
// or a slightly different syntax (which generates different JavaScript):
export var Delete = (id: number) => {
};
}
You can't have the class named User and the function also be User, so I've changed it to GetUser in the example.
You could then call:
Api.GetUser(1234)
or
Api.Delete(1234);
Option 2: Using an interface
You could also approach this by using an interface if you wanted to limit the ability of
calling code from being able to instantiate instances of the inner class by using an interface instead. Below I've created a simple ISuperUser interface and and implementation of the class called SuperUserImpl. As the SuperUserImpl isn't exported, it's not publicly creatable. This is nice in that you could use simple Api.SuperUser(2345) to return new instances of a class that implements the ISuperUser interface.
module Api {
export interface ISuperUser {
id: Number;
name: String;
}
class SuperUserImpl implements ISuperUser
{
constructor(public id: Number) {
}
public name: String;
}
export var SuperUser = (id:Number):ISuperUser => {
return new SuperUserImpl(id);
}
}
var su : Api.ISuperUser = Api.SuperUser(5432);
alert(su.id);
Option 3: JavaScript and instanceof
There's a trick that is often used in JavaScript class constructor wherein the function/constructor for a new object checks to see whether it is of the right type (was the function called or created), and if not created, returns a new instance:
if (!(this instanceof User)) {
return new User(id);
}
While correct, TypeScript when trying to call a constructor with that will cause a compiler warning. This will work, but with the compiler warning:
constructor(id: Number) {
if (!(this instanceof User)) {
return new User(id);
}
}
And later calling:
var u: Api.User = Api.User(543);
A compiler warning suggests, "did you forget to use 'new'?" It does produce valid JavaScript, just with a warning. I'd probably go with the static-like method approach to avoid the TypeScript compiler warning.

Categories

Resources