ES6 code completion (Intellisense) with composition in VSCode - javascript

I am trying to get code completion to work when nesting classes in es6 using the following syntax: class Dog extends FoodMixin(Animal). The first implementation works fine, giving me auto completion for both FoodMixin and Animal. However, if I nest it one deeper, or apply a second mixin, the auto completion stops. For instance: class Dog extends OtherMixin(FoodMixin(Animal)) will lose the code completion for the FoodMixin class.
Is there a way I can have intellisense work for both OtherMixin and FoodMixin?
Simple test code:
const FoodMixin = superclass => class extends superclass {
eat() {
console.log("Eating");
}
};
const OtherMixin = superclass => class extends superclass {
test() {
console.log("Hello");
}
};
class Animal {
}
class Dog extends OtherMixin(FoodMixin(Animal)){
}
const dog = new Dog();
dog.test(); //INTELLISENSE WORKS
dog.eat(); //INTELLISENSE DOES NOT WORK

VS Code's JavaScript IntelliSense will not be able to understand very dynamic code like that example. The tutorial you linked to that provided this code is being too clever for its own (or really anyone's) good.
However you can sort of workaround VS Code's limitation by adding explicit type annotations for the types and interfaces used in the example using JSDoc:
/**
* #typedef {{ eat(): void }} Eater
*/
/**
* #typedef {{ test(): void }} Tester
*/
/**
* #type {Dog & Eater & Tester}
*/
const dog = new Dog();
dog.
The & is not standard JS Doc type syntax but an intersection type from TypeScript. (I am using it here because it mimics composition)

Related

How can I make TypeScript aware that `classInstance.constructor` in JS will give access to the class' static methods and members?

The following code works in plain JavaScript:
class Post {
static table = "Posts"
title = ""
}
const post = new Post()
console.log(post.constructor.table) // will log "Posts"
But for some reason, TypeScript does not seem to be aware that the constructor member of a class instance gives access to that class' static methods and members. TypeScript will insist that post.constructor is nothing more than the same function as Object.constructor.
This is problematic in a project that I'm working on. So I wonder, is there a way to make TypeScript aware that post.constructor.table does actually exist?
I have found a way that works, but it is super ugly and I really hope there is another way.
import type { Class, Constructor, EmptyObject } from "type-fest"
// Here I'm overriding the constructor function on a class instance type,
// because in plain javascript it's also possible to access
// the constructor function from a class instance and then
// get access to all the class' static methods and members.
type OverrideConstructorToIncludeStatic<
T,
Static = EmptyObject,
Arguments extends unknown[] = any[],
> = T & { constructor: Static & Constructor<T, Arguments> }
// This augmented Class type allows one to define
// expected static methods and members on classes.
type ClassWithStatic<
T,
Static = EmptyObject,
ConstructorArguments extends unknown[] = any[],
> = Class<
OverrideConstructorToIncludeStatic<T, Static, ConstructorArguments>,
ConstructorArguments
> & Static
type HasTable = { table: string }
type ClassHasTable<T> = ClassWithStatic<T, HasTable>
function extend<T>(classType: Class<T> & HasTable): ClassHasTable<T> {
return classType as ClassHasTable<T>
}
const Post = extend(class Post {
static table = "Posts"
title = ""
})
const post = new Post()
post.constructor.table // now TypeScript won't complain
So that works, but const Post = extend(class Post { ... }) is so ugly. And moreover, I'm not the one who would need to write that. The consumers of my library would need to write that and I don't want to do that to anyone.
Is there any other way of doing this?
Edit
Of course I'm using over-simplified examples here. In case you'd like to know the real use-case I'm asking this for, you can check out this playground: https://tsplay.dev/wXqeDN.
In short, I'm trying to see if I can figure out a way to (un)serialize class objects that adhere to a certain interface requiring static methods for the (un)serializing process. The end goal would be that I could serialize class objects in a backend and unserialize them in frontend code.
But I just realized that that might not be possible after all, because I would probably also need to serialize the classRegister Map you see in that playground. And I'm afraid that may prove to be impossible. Unless I'd use something like eval, but we all know how dangerous that is.
Corrected thanks to caTS's observation
Since constructor properties are not available you have to do something more or less ugly.
Your code can be made more general by getting the static properties of any class.
A variant using type casting might be:
type ClassLike = { new(...args: readonly unknown[]): unknown }
type InstanceTypeWithConstructor<C extends ClassLike> =
{
constructor: C
}&
InstanceType<C>;
class Post {
static table = "Posts"
title = ""
}
const post = new Post() as InstanceTypeWithConstructor<typeof Post>;
const consTable = post.constructor.table;
playground link
The first version of my answer was erroneously removing prototype from the properties of constructor using key remapping
-- playground link

Javascript - "Compound" Class Inheritance

Not to be confused with inheriting from multiple classes.
Suppose there's a class, Player, with its own methods and properties:
class Player {
//...
}
Next, suppose there's a class which extends from Player, with its own special properties
class SpecificPlayer extends Player {
constructor() {
super();
this.foo = "bar";
}
}
Now suppose there's another class, Core, with a property of type Player[] (types are defined through JSDoc-style comments in VSCode), and a method which returns a portion of that array.
class Core {
constructor(players) {
/** #type {Player[]} */
this.players = players;
}
getSomePlayers() {
return this.players.filter(...);
}
}
Finally, let there be a class which extends from Core, with a constructor and method as follows:
class SpecificClass extends Core {
/** #param {SpecifcPlayer[]} players */
constructor(players) {
super(players);
}
doThing() {
console.log(this.players[0].foo);
this.getSomePlayers().forEach(p => console.log(p.foo));
}
}
Now, if the following main body code is ran:
const SpecificInstance = new SpecificClass([new SpecificPlayer(), new SpecificPlayer()]);
SpecificInstance.doThing();
Everything is fine and dandy -- "bar" is printed successfully for all log statements.
However, when writing the code (specifically the body of doThing), VSCode Intellisense would only recognize this.players as a type of Player, meaning it wouldn't recognize the property foo. How would I go about writing the classes such that the VSCode autocomplete/Intellisense recognizes the correct type of this.players, and each method in Core which uses this.players also returns the correct type of SpecificPlayer? Realistically, there are several classes extending from Core, some with their own special variation on Player. As such, the types returned should be that which was provided originally to the constructor.
(Or is there a better way to organize/format my code?)

Typescript: Adding additional overloads to an existing abstract class

I am working on developing a set of definitions for DefinitelyTyped, for the package Leaflet.Editable. Leaflet is notable in that it uses a partially custom class implementation to allow for extending of existing types in pure JavaScript, as seen here. Simply calling import 'Leaflet.Editable' anywhere in your code will add new functionality to the existing Leaflet classes, such as the ability to enable and disable editing of certain layers.
It is not implemented in TypeScript, and as such typing is implemented in #types/leaflet, so the package I am developing (#types/leaflet-editable) has to import the leaflet namespace and extend the existing types.
I must extend the existing types rather than adding new ones because there are other libraries (such as react-leaflet) that use those types.
For example, by extending the MapOptions type (by merging the interfaces), I was able to add new properties to react-leaflet's MapComponent component, since its props extend Leaflet.MapOptions. If I were to create a new type, EditableMapOptions, it would not be extended by react-leaflet and thus I would not be able to add those properties to MapComponent.
I was able to extend several existing interfaces, and add new ones such as VertexEventHandlerFn (which represents an event callback that provides a VertexEvent).
The issue is how #types/leaflet implements the map.on('click', (event: LeafletEvent) => {}) function. Here's a snippet from the DefinitelyTyped repo:
export abstract class Evented extends Class {
/**
* Adds a listener function (fn) to a particular event type of the object.
* You can optionally specify the context of the listener (object the this
* keyword will point to). You can also pass several space-separated types
* (e.g. 'click dblclick').
*/
// tslint:disable:unified-signatures
on(type: string, fn: LeafletEventHandlerFn, context?: any): this;
on(type: 'baselayerchange' | 'overlayadd' | 'overlayremove',
fn: LayersControlEventHandlerFn, context?: any): this;
on(type: 'layeradd' | 'layerremove',
fn: LayerEventHandlerFn, context?: any): this;
...
As you can see, it uses an abstract class that implements a generic definition for when users include multiple events separated by spaces, as well as several specific events that, when specific arguments are provided, include proper typing for their event handlers.
I wanted to add my functions to this, but I cannot find any means to add additional overloads to an existing abstract class. The thing to note here is that all these overloads of on are implemented by the same line of code in Leaflet, so the issues faced by adding new methods to an abstract class (that would later go unimplemented by the associated JavaScript) do not exist here.
I tried simply attempting to redeclare abstract class Evented to add more methods to it but TypeScript tells me it's already defined. My research only found the TypeScript documentation indicating that I should utilize mixins, which I cannot do because I need to modify the existing class, and creating a new one would not solve the problem.
Here is my current TypeScript implementation of leaflet-editable so far.
It might not be clear from the documentation for module augmentation, but if you want to merge into the instance type of a named class, you can do so by adding to the interface with the same name as the class. When you write class Foo {}, it treats the type named Foo as an interface, and you can merge into it. For example:
import { Draggable, LeafletEvent } from 'leaflet';
interface PeanutButterEvent extends LeafletEvent {
chunky: boolean
}
declare module 'leaflet' {
interface Evented {
on(type: "peanut-butter", fn: (x: PeanutButterEvent) => void): this;
}
}
const draggable = new Draggable(new HTMLElement()) // whatever
draggable.on("peanut-butter", e => console.log(e.chunky ? "Chunky" : "Creamy")); // ok
draggable.on("resize", e => console.log(e.oldSize)); // still works
Here I've added another overload of on() to the interface named Evented.
On the other hand, if you want to merge into the static type of the class, you can do so by adding to the namespace with the same name as the class. When you write class Foo {}, it treats the value named Foo as a namespace, and you can merge into it. For example:
import { Draggable, Evented } from 'leaflet';
declare module 'leaflet' {
namespace Evented {
export function hello(): void;
}
}
Evented.hello = () => console.log("hello"); // implement if you want
Draggable.hello(); // okay
Draggable.mergeOptions({}); // still works
Here I've added another static method named hello() to the namespace named Evented.
Playground link to code

Implementing an interface in Javascript using flow-runtime

I am using flow-runtime plugin for babel to generate dynamically typechecked javascript code. The following is the workflow I am using
write static javascript code (with flow annotations)
compile this code using babel to convert flow annotations to typechecked code
run this compiled code in node.js
The following workflow gives me an ability to write typescript type code, but with type checking only where I want.
So, now that we understand what I am doing, let me explain what I am trying to achieve
I basically need to build a class called Interface, which will do exactly what it sounds like. This class will be extended by classes that are supposed to be interfaces, and then extended by other classes. Something like this :
class Interface() {
constructor() {
...
}
// interface superclass, supposed to be extended by all interfaces
// this class will provide all the utility methods required
// by an interface, such as validating the implementation of the
// interface, ...
validateInterfaceImplementation() {
...
}
}
// interface definition
class FooInterface extends Interface {
constructor() {
super();
...
}
}
// actual class, that will implement the "FooInterface" interface
class Foo extends FooInterface {
constructor() {
super();
...
}
}
Now, I want to enforce strict implementation of the FooInterface. That means that I want a way to define all the methods that the FooInterface interface expects to be implemented, and validation that all these methods have been implemented by the Foo class.
What I have tried looks something like this
// interface.js
// #flow-runtime
class Interface<T> {
constructor(t: T) {
(this: T); // let flow validate that the interface is implemented
}
}
// FooInterface.js
// #flow-runtime
type foInterface = {
bar(param: string): number;
}
class FooInterface extends Interface<fooInterface> {
constructor() {
super(fooInterface);
}
}
// Foo.js
// #flow-runtime
class Foo extends FooInterface {
}
new Foo(); // should throw an error, because the interface is not implemented
// (the function bar is not defined)
I am facing multiple problems with this approach
I am not sure how to implement the generic class Interface<T>. In think my implementation is incorrect, and the compiled babel code also throws an error, but I can't figure out how to do this.
I am not even sure whether this method will work or not, or whether this is the best way to approach this problem.
Any help would be welcome. Thanks in advance :)
As of flow-runtime 0.5.0 you can use Flow's implements keyword combined with Flow interfaces. I think this will give you what you want without having to create the concrete classes at all:
// #flow
// #flow-runtime
interface IPoint<T> {
x: T;
y: T;
}
interface IJSON {
toJSON (): any;
}
class Point implements IPoint<number>, IJSON {
x: number = 123;
y: number = 456;
toJSON () {
return {x: this.x, y: this.y};
}
}

Typescript definition for ES6 mixins

Is there a way to write a Typescript definition for an ES6 mix-in?
I've this pattern in library.js, and I'd like to create the library.d.ts
// declaration in `library.js`
class Super extends Simple {
constructor() {}
static Compose(Base = Super) {
return class extends Base {
// ...
}
}
}
// usage in `client.js`
class MyClass extends Super.Compose() {}
let myInstance = new MyClass();
class MyOtherClass extends Super.Compose(AnotherClass) {}
No, Typescript type system is not expressive enough for that - see the discussion in https://github.com/Microsoft/TypeScript/issues/7225 and https://github.com/Microsoft/TypeScript/issues/4890.
The idiomatic 'type of classes' in typescript is written as
interface Constructor<T> {
new (...args): T;
}
So one way to write declaration for Compose is
export declare class Simple {}
export declare class Super extends Simple {
static Compose<T>(Base?: Constructor<T>): Constructor<T & {/*mixed-in declarations*/}>
}
That is, Compose return type is declared to be a constructor for intersection type - a type which must have all the properties of parameter (Base) together with all the properties of the mixin.
You can use that declaration (assuming it's in the library.d.ts file) like this
import {Super} from './library'
let MyComposed = Super.Compose(Super)
let myInstance = new MyComposed
The minor inconvenience is that you always have to provide argument for Super.Compose() because type inference does not work without knowing the value for default parameter, and you can't provide value for default parameter in the declaration file.
But the big problem is that you can't really use the result of Compose as a class:
class MyClass extends Super.Compose(Super) {}
does not compile due to the issues mentioned above:
error TS2509: Base constructor return type 'Super & {}' is not a class or interface type.

Categories

Resources