-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
A way to specify class properties using JSDoc #26811
Comments
@sandersn can this be accomplished with declaration merging today? |
Is the ideal solution a list of possible properties, or can the DTO truly hold any property that will come along? There are a couple of workarounds you can use:
interface DTO {
[key: string]: any
} Merging happens automatically in scripts (since they're all global), or you will have to wrap the interface in As you point out,
class C {
/** @type {Object?} */
otherProperty;
constructor(id) {
this.id = id;
}
} This feature isn't done yet either, but is tracked by #27023. |
I'm sorry, I'm probably missing something. If I create a I'm not that well-versed in TypeScript, so I'm feeling a little lost here... |
Yes, you are correct, X.d.ts and X.js causes X.d.ts to shadow the JS file. If you are using a tsconfig.json (or jsconfig) then you need to make sure both the js and d.ts are included in the files list or glob. Otherwise, you'll need an explicit I'm not actually sure whether CommonJS needs to use module augmentation syntax in order to merge with .d.ts interfaces. Let me check... |
Bad news. CommonJS modules do indeed require matching module augmentation syntax, but it does not work right now. Here's what I got to work in Typescript: // @Filename: ex.d.ts
export var dummy: number;
declare module "./welove" {
interface DTO {
// (number is more obvious when it works)
[s: string]: number
}
}
// @Filename welove.ts
export class DTO {
constructor(public n: number) { }
}
var c = new DTO(1)
c.n // ok
c.w // ok Unfortunately, the same thing doesn't work with commonjs: // @Filename: ex.d.ts
export var dummy: number;
declare module "./test" {
interface DTO {
// (number is more obvious when it works)
[s: string]: number
}
}
// @Filename test.js
class DTO {
/** @param {number} n */
constructor(n) {
this.n = n
}
}
var c = new DTO(1)
c.n // ok
c.w // error
module.exports = { DTO } I'll file a bug to make sure this works. At this point, I think we have 3 proposals to solve this problem. Unfortunately, none of them work right now! |
Never mind. Thanks to @andy-ms for pointing out that the distinction is not TS vs JS, but ES exports vs CommonJS value exports. That is, this works: // @Filename: ex.d.ts
export var dummy: number;
declare module "./test" {
interface DTO {
[s: string]: any
}
}
// @Filename test.js
module.exports.DTO = class {
/** @param {number} id */
constructor(id) {
this.id = id
}
}
var c = new module.exports.DTO(1);
c.id // ok
c.w // ok But my previous example, with |
any update on this? want to use jsdoc to add optional prop of a class, currently it does't respect optional flag. class DTO {
/**
* @type {string?}
*/
other;
} |
The JSDoc @type docs say a "nullable type" is |
Originally posted by @zzswang in #26811 (comment)
You need to write: class DTO {
/**
* @type {string | undefined}
*/
other;
} Note that class instances will have the |
Is it possible somehow to make this work with ES6 export/imports? What I am trying to achieve, is to "extend" an exported class JavaScript class, with overriden methods from a This is what I am trying:
If I omit the |
Here is my use case, which involves somersaults through the hoops of making good JSDocs for Proxy declarations in pure JavaScript. The problem here is that I need the following things to work simultaneously:
Class constructor version
Factory version using empty class
Factory version using @typedef
Why I don't like the workarounds
|
…ypeScript#26811" This reverts commit 5a7292e.
needed due to this issue: microsoft/TypeScript#26811
needed due to this issue: microsoft/TypeScript#26811
needed due to this issue: microsoft/TypeScript#26811
I've been trying to figure out how to accomplish this. Was there any resolution? I just want to document that at runtime my class will have a "property" of "Type"... I could have sworn there was a way to document these properties. |
Note that this syntax, class DTO {
/**
* @type {string | undefined}
*/
other;
} introduces runtime behavior that will break code because the Here's an example of how it breaks. This is the plain JS code: class Base {
constructor() {
this.init()
}
}
class Subclass extends Base {
init() {
this.other = 123
}
}
console.log(new Subclass().other) // logs "123" Now we try to add types with JSDoc and it breaks: class Base {
constructor() {
this.init()
}
}
class Subclass extends Base {
/** @type {number} */
other;
init() {
this.other = 123
}
}
console.log(new Subclass().other) // logs "undefined" We need the equivalent of I understand we can refactor code to make it work, but in some cases that may be a lot more work, especially in rather large plain JS projects. |
A similar syntax works in constructors: class C {
constructor() {
/** @type {number | undefined} */
this.p
}
} |
I THINK I might have found a way to solve some of this (at least as far as Webstorm intellisense is concerned) - excuse the shitty example This does not work. // base classes
class Wheel {
rotate() {}
}
class Machine {
/** @type {Wheel[]} */
wheels;
}
// derived classes
class CarWheel extends Wheel {
rotateOnRoad() {}
}
class Car extends Machine {
constructor() {
super();
this.wheels.push(new CarWheel());
}
drive() {
this.wheels.forEach((wheel) => {
// this gives me an unresolved function warning as rotateOnRoad() does not exist on the base wheel class.
wheel.rotateOnRoad();
});
}
} But if I update the JSDOCS on my derived class with @typedef and @Property, I can redeclare the type of the inherited property without having to actually redeclare the property on the derived class (which would break the property at runtime). /**
* @typedef {Car} Car
* @property {CarWheel[]} wheels
*/
class Car extends Machine {
constructor() {
super();
this.wheels.push(new CarWheel());
}
drive() {
this.wheels.forEach((wheel) => {
// this now works as the type the iterate from this.wheels has been updated to {CarWheel}
wheel.rotateOnRoad();
});
}
} |
Causes a duplicate identifier error on VS Code |
I'm moving to supporting a more functional approach to my Web Components. When I'm working with a class, I can tap into export default class ExtendedFab extends Button {
static { this.autoRegister('mdw-extended-fab'); }
compose() {
return super.compose().append(styles);
}
}
// `.prototype.lowered` is actually useless in code. `idl()` calls defineProperty()
// Typescript picks up .prototype calls and adds it as a class property.
ExtendedFab.prototype.lowered = ExtendedFab.idl('lowered', 'boolean'); This is now the new syntax I want to support: const ExtendedFab = Button.extend(styles);
ExtendedFab.autoRegister('mdw-extended-fab');
ExtendedFab.prototype.lowered = ExtendedFab.idl('lowered', 'boolean');
export default ExtendedFab; It doesn't work because while Typescript does understand It seems it's splitting there. I wonder if there's a way to strip that type, but I haven't found it. |
Want support for either: /** @property {string} id */
class DTO {
constructor(obj) {
Object.assign(this, obj)
}
} or class DTO {
/** @property {string} id */
constructor(obj) {
Object.assign(this, obj)
}
} This one do not cut it... it introduce different runtime behavior class DTO {
/** @type {number} */
id
} |
I've spent the last week or so writing small runtime cast functions. I've manage to get it to work with extended classes as well as mixins. I was holding off until it's done, but seeing @jimmywarting here 👋 , I might as well share my progress. I was able to get property casting working with this: /**
* @template T Class
* @template {Object} P
* @typedef { Omit<T, 'prototype'> &
* {
* new (...args:ConstructorParameters<T>): InstanceType<T> & P
* } & {
* prototype: InstanceType<T> & P
* } } DefinedPropertiesOf
*/
/**
* @template T Class
* @template {Object} P
* @param {T} Class
* @param {P} props
* @return {DefinedPropertiesOf<T,P>}
*/
const CastDefinedPropertiesOf = (Class, props) => Class; With this, you can use what basically minifies down to a couple of bytes and TS will accept your casted input. With this, I was able to work out a functional syntax like this: export default ExtendsMixin(Button, { lowered: 'boolean' })
.compose(styles)
.autoRegister('mdw-extended-fab'); With ( I was able to create some other types with this "useless" function calling method, which are /**
* @template T
* @typedef {{
* new (...args:ConstructorParameters<T>): InstanceType<T>
* } & {
* prototype: InstanceType<T>
* } & {
* [P in keyof T]:T[P]
* }} ClassOf
*/
/**
* @template T
* @param {T} Class
* @return {ClassOf<T>}
*/
const CastClassOf = (Class) => Class; And use |
damm, that's a lot for just simply using existing jsdoc |
@jimmywarting 100% agree. But I don't think we have a JSDoc champion here anymore. I've kinda given up on waiting on better JS support with typecheck. I'm tired of searching and finding my own comments from years/months ago 😞 . I've resigned to just having to build a solution myself, even if it needs a runtime cast. I expanded your sample a bit with an improved typedef that shouldn't invoke TS warnings. // TS Casts
/**
* @template {abstract new (...args: any) => any} T Class
* @template {Object} P
* @typedef { Omit<T, 'prototype'> &
* {
* new (...args:ConstructorParameters<T>): InstanceType<T> & P
* } & {
* prototype: InstanceType<T> & P
* } } DefinedPropertiesOf
*/
/**
* @template {new (...args: any) => any} T Class
* @template {Object} P
* @param {T} Class
* @param {P} props
* @return {DefinedPropertiesOf<T,P>}
*/
const CastDefinedPropertiesOf = (Class, props) => Class;
// Code section
class DTO {
constructor(obj) {
Object.assign(this, obj)
}
}
const DTO_SCHEMA = {id:''};
const DefinedDTO = CastDefinedPropertiesOf(DTO, DTO_SCHEMA);
// Export Cast instead of Class. Same runtime, parsed differently by TS.
export default DefinedDTO;
// Tests
const instance = new DefinedDTO();
instance.id = 'abc';
// @ts-expect-error Invalid boolean => String
instance.id = false; You can also do: DTO.prototype.id = /** @type {string} */ (undefined); But, I've personally modified my If anybody wants to try to add it into the TS code, I believe it would here at https://github.com/microsoft/TypeScript/blob/4932c8788b9a0737d54a17a1d7c613b8f4daa6c2/src/compiler/checker.ts at lines 11346, with the function The type checker is a bit daunting at 47,000 lines for one file, so I'm not really ready to jump into such a large file and start working on it. I rather do a |
Search Terms
JSDoc class properties any key
@property
Suggestion
I would like to be able to extend the type of an ES6 class using JSDoc comments, such that TypeScript understands there are other properties that might be added to the object later.
Alternatively, I would like a way to indicate that a given class acts as a container that can contain any possible key.
Use Cases
I have a class that is used like a DTO. It has some properties that are guaranteed to be there, but is also used for a lot of optional properties. Take, for example:
TypeScript now recognizes the object type, but whenever I try to use other properties, it complains. Currently, I'm resorting to something like this as a work-around:
But it's ugly and verbose, and worse, it includes actual JavaScript code, that serves no purpose other than to provide type information.
Examples
Rather, what I would like to do is something like this (that I would like to be equivalent to the snippet above):
Another equivalent alternative could be (but currently doesn't compile because of a "Duplicate identifier" error):
Or, otherwise, some way to indicate this class can be extended with anything. Which means I would like a way to specify the following TypeScript using JSDoc and I want it to apply to an existing ES6 class (because I do use the class to be able to do
instanceof
checks):Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: