Repository navigation
Feature Request: Documented Utility Type #41165
Description
Search Terms
Docs, documentation, re-key, utility, type, extract, literal, update, tsdoc, comment, source, text
Suggestion
I'd love to see documentation as a first-class citizen of the type system. My suggestion is a new utility type Documented, which allows users to document and unwrap documentation at the type-level.
The type could look as follows: Documented<V, D extends string> = V.
This:
type X = {
a: Documented<string, "Some documentation for `a`">;
};... would be equivalent to this:
type X = {
/**
* Some documentation for `a`
*/
a: string;
};While it would be preferable for consistency to specify the documentation attached to the key (as this is how mapped types transfer documentation), index signature and other contextual restrictions apply. Aka., this is no good:
type X = {
[Documented<"a", "Some documentation for `a`">]: string;
}Additionally, accessing the symbol of the literal of a specific key can be arduous (Extract<keyof ..., ...>). Whereas accessing the symbol of the field is simple (X["a"]).
ALSO: apologies if I'm butchering the terminology.
Use Cases
This utility type would enable users to extract documentation as string literal types, which they could then further manipulate and use elsewhere. They could build up higher-order generic types to assist with and type-check the documentation process.
Examples
type X = {
/**
* Some documentation for `a`
*/
a: string;
};
type XaDocs = X["a"] extends Documented<any, infer D> ? D : undefined;
declare const xaDocs: XaDocs; // "Some documentation for `a`"
// update (prefix) the string literal type of the documentation
type XaDocsUpdated = XaDocs extends string ? `USE WITH CAUTION: ${XaDocs}` : undefined;
// utilize the `Documented` utility to attach the new documentation
type XUpdated1 = {
a: Documented<string, XaDocsUpdated>;
};
// perhaps within a mapped type
type XUpdated2 = {
[K in keyof X]: K extends "a" ? Documented<X[K], XaDocsUpdated> : X[K];
};In either of the cases above, a will be documented as "USE WITH CAUTION: Some documentation for a".
The real power of this utility is not in direct usage as demoed above, but rather in higher-order utilities. For instance, you could imagine a utility library that lets users specify flags that correspond to expected documentation.
import {EnsureDocumentationExists} from "some-doc-utility-lib";
import {HasDescriptionAndExampleDocs, HasDescriptionDocs} from "./my-own-potentially-poorly-documented-types";
export type A = EnsureDocumentationExists<HasDescriptionAndExampleDocs, {description: true; example: true}>;
export type B = EnsureDocumentationExists<HasDescriptionDocs, {description: true; example: true}>; // type-error
export type C = EnsureDocumentationExists<HasDescriptionDocs, {description: true}>;Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
Activity
sam-goodwin commentedon Oct 19, 2020
Documentation can be bound to both types and properties:
How would they be differentiated?
harrysolovay commentedon Oct 19, 2020
We could create a differently-documented
Ainterface like so:Or create the type from scratch.
harrysolovay commentedon Oct 20, 2020
It just occurred to me, this would be extremely useful for developers of type-level DSL parsers (which I'm guessing is the next wave of TS tooling thanks to ^4.1's string literal parsing).
I've been experimenting with parsing GraphQL source string types into GraphQL AST types (and soon hopefully into TS types). For now, it's tough not to provoke the recursion limiter. A source string with more than a few statements gives TS error 2589. Soon, however, I'd hope it will be possible to parse and extract type information from large samples of many embedded languages.
So how could we make use of GraphQL documentation ("block strings")? Ideally we could extract that documentation from the GraphQL source string type, and reuse it for js/tsdocs.
Let's look at this hypothetical GraphQL-in-TS experience:
Here, we would want for the documentation in the GraphQL source string to flow through into
Schema.A
Documentedutility type would enable the creation of such an experience.micnic commentedon Oct 21, 2020
I might be mistaken, but comments are not actual values in JS/TS or any other programming language and therefore they cannot have a type in anyway, so the syntax you propose is impossible to implement.
harrysolovay commentedon Oct 21, 2020
Nicu Micleușanu (@micnic) I'm not sure why having or not having a JS value complement would impose whether a syntax is possible. For instance, type params which act as type variables for reusability in others.
I'm curious to hear your thoughts on the following arguments for a
Documentedtype:Js/tsdoc comments are already treated (somewhat) as members of the language. They abide by a specific control flow so that users can re-alias and map between documented types with their documentation preserved. Another similarity with the language: documentation is not critical to the code's runtime execution and can be compiled away (just like static types).
Documentation is a pillar of good software. Yet it's often difficult to give this process the time of day. This is––in part––because documentation cannot be easily modularized, composed, and constrained (not to mention, it involves configuring often-custom workflow tools, which spawn new processes). These challenges can be addressed well within the language. Existing documentation validation tools' flexibility pales in comparison to that of the type system. A
Documentedutility type bridges the divide.tjjfvi commentedon Oct 22, 2020
A few days ago I submitted #41023, which has a similar intent. If this were to be implemented I would suggest a
Documentation<T>(or similar) type, that could be used likeDocumented<T, Documentation<U>>, to accomplish theT & Docs<U>I had suggested there.As a slight aside, I don't really care for the
Documented<T, "blah">, and would advocate for either:T & Docs<"blah">andT & DocsOf<U>orWithDocs<T, "blah">andWithDocs<T, DocsOf<U>>harrysolovay commentedon Oct 22, 2020
T6 (@tjjfvi) it does seem that we're requesting the same thing! Apologies for not finding your issue.
Your proposed solution is certainly more readable!:
Yours
Vs.
If we intersect with
Docs<"blah">, the result contains a new field (the documentation string keyed by a unique symbol). This means that theDocsutility type would look something like this:type Docs<S extends string> = {[docsKey]: S}.If we were to document some value...
And then utilize that type...
We'd get a type error.
Would the error––in the case of the
Docsutility––be suppressed? (doesn't seem like the right path)There's also the matter of js/tsdoc -> types.
Does the signature of
Abecomenumber & Docs<"Some description.">? (also doesn't seem like the right path)What I proposed is also flawed, as
Documentedis essentially an identity, and the documentation string type isn't accessible on the type it's documenting (only through unwrapping withinfer). Nothing else in the type system behaves this way. (aka. this also doesn't seem like the right path... but maybe documentation is the exception)At this point, I have no strong preference for the experience. Just a belief that manipulating documentation within the type system could open some cool & useful doors.
Hopefully we get some more feedback and ideas.
tjjfvi commentedon Oct 22, 2020
In my mind, the
Docs<T>would be "magic" type likeThisType<T>; inlib.d.ts, it would be written astype Docs<T> = unknown, but would receive special casing in the compiler to add the jsdoc and handle the intersection properly.No; in my mind, both
AandB(below) would display asnumber, but with theSome description.displayed below like it is currently forA.Playground Link
Originally, I thought that the the
DocsOf<T>utility I proposed would be handled with anintrinsicutility type (#40580). However, using conditional types and infer to unwrap it is pleasing, and would be one less "magic" type. Thus, I would likely advocate for theDocsOf<T>to be defined inlib.d.tsasT extends Docs<infer D> ? D : never.IMO, the special handling makes sense for documentation; it isn't really type information, as
T & Docs<"A"> === T & Docs<"B">. I could however see an argument that theDocsOf<T>violates this, as it takes "not really type information", and converts it into a literal string type; if this was a concern, I would propose theDocsOf<T>be a similar magicunknownwith special handling, and it would be used likeA & DocsOf<B>.While I think people may be resistant to bringing documentation information into the type system, I think it is essential with the complex types typescript is increasingly allowing us to create, as many of these complex type modification lose documentation, like I demonstrated in #41023.
harrysolovay commentedon Oct 22, 2020
Yes!: it would be an intrinsic utility type, so I suppose there's no need to get too fixated on the "magic" as bad. One more question about your proposal:
In this case,
aAndBdoesn't contain the documentation of the right-hand of its intersection.While introducing an intrinsic utility makes sense... I'm not so sure that its usage should enforce a convention for applying the documentation data of
Docs<"...">.If this is the desired experience, I'd hope that––in the above example––
aAndBwould contain the docs ofA. However, what then do we do in this situation?:This leads me to believe that you're proposing some new documentation control flow behavior in addition to the intrinsic utility type. Is this the case?
tjjfvi commentedon Oct 23, 2020
No; rather I was suggesting a special casing wrt intersection types and the
Docs<T>/DocsOf<T>, so thatA & Docs<"blah">would transfer the documentation, whileB & AWithDocswould not.I agree that this inconsistency seems non-ideal, but I think it is reasonable:
A & Docs<"test">reads cleaner thanDocumented<A, "test">This is the "magic" I was talking about in my earlier comments; similar to how
ThisTypeis handled specially.tjjfvi commentedon Oct 24, 2020
See also rbuckton's recently submitted #41220
tjjfvi commentedon Oct 25, 2020
So, after digging around for a while in typescript's source, I have started to figure out how the documentation stuff gets handled.
AFAICT, when it is retrieving the documentation, it:
getQuickInfoAtPositioninsrc/services/services.tsSymboland notSymbolObject, but I didn't see any instance of this in the source)return []SignatureObject.getDocumentationCommentgetDocumentationComment([this.declaration], this.checker)SymbolObject.getDocumentationCommentgetDocumentationComment(this.declarations, checker)SymbolObject.getContextualDocumentationCommentget/setstuff (I think).getDocumentationCommentI attempted to start inspecting what the
getDocumentationCommentfunction was being passed, but have not yet been able to get it working.kaya3 commentedon Mar 17, 2022
This could perhaps be implemented by creating a unique symbol type (e.g.
DocComment), and declaring:The changes to the compiler required would then be:
DocComment,DocCommentproperties on the type, andDocCommentproperties and instead show them as documentation.In the meantime, you can get something close to the desired behaviour in your own projects without compiler support, by declaring the
DocCommentsymbol yourself, and tolerating the less-ergonomic IntelliSense experience.Playground Link
tjjfvi commentedon Mar 17, 2022
That wouldn't work, because it would mean that
Further, adding a property to every type with documentation would be a breaking change, as it would also affect things like
keyof.harrysolovay commentedon Mar 25, 2022
Another use case: mapping a TS type into a constraint for a JSON schema. For instance, imagine a
JsonSchemaOfutility type:The
JsonSchemaOfutility type could leverage aDocumentedutility type for extracting metadata, such as default values.Without the
Documentedutility type, we must encode the constraint in less desirable ways...... this of course requires that we then remove the attached metadata before using elsewhere...
Anyways, just thought I'd share this additional use case. Still very much hoping the
Documentedutility type is given consideration. Thanks!entropitor commentedon Mar 27, 2022
Harry Solovay (@harrysolovay) Isn't the above comment the reverse of this ticket? Being able to access documentation of a type (latest comment) instead of being able to attach documentation to a type (the original issue)?
tjjfvi commentedon Mar 27, 2022
Jens Claes (@entropitor) The issue is about both, to a degree; see this example from the original post:
AlCalzone commentedon Jun 13, 2022
I have another use case for this. In a library of mine, I am defining an extensive object through a high level abstraction to avoid a lot of boilerplate code. For example, this input:
which is being defined in a similar way in over 50 other files with more or less definitions results in an object with the following type, all created through some generic type magic:
While this is very helpful at the usage site, being able to preserve the comments as JSDoc on the resulting object properties
nodeIdsetc. would be a big improvement.Just the comments would be enough in my case, I don't see a need for JSDoc tags at the moment.
magwas commentedon Mar 11, 2023
Martin Johns (@MartinJohns) I am confused about whether and how this proposal would allow documentation generated by external tools to be inserted such that it appears at the IDE at the end of the day, as of the motivation of #53205
For example if I have
I could of course generate a barrel like
but I would rather not get involved in generating the barrel, as my responsibility is to check contracts and document them, not determining what will be exported or not.
something like the below would be okay, but I do not see how
MagicallyInsertDocumentationwould work, and what other steps should be done to actually have the comment in the IDE.