Skip to content

Use npm @types/* for 3rd party type definitions #310

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

Merged
merged 6 commits into from
Oct 20, 2016

Conversation

christopherthielen
Copy link
Contributor

This PR removes the typings files from typedoc/src/typings in favor of using npm @types/* packages.

It now (automatically) uses the official typescript.d.ts definitions from node_modules/typescript/lib/typescript.d.ts.

TypeDoc uses quite a few internal TypeScript APIs. Those APIs were exposed in the (old?) typescript.d.ts from DefinitelyTyped.
To accommodate TypeDoc's existing use of internal TypeScript APIs, I created a ts-internal.ts file. The file augments the official type definitions with the internal APIs that we are using.
Obviously it would be best not to use the internal APIs at all. This first pass can be used to identify which internal APIs we are using, so we can work towards replacing those uses with official API.

 - Use the latest `typescript.d.ts` definitions
 - Add `ts-internal.ts` to expose the internal Typescript APIs (that TypeDoc is currently using) using declaration merging
 - Remove tsd and typings files

Closes TypeStrong#309
Copy link
Collaborator

@aciccarello aciccarello left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like all these changes, especially pulling out the internal TS apis that are used. Glad to see people helping move this forward 👍

declare module "shelljs" {
// `stdout` was added in:
// https://github.com/shelljs/shelljs/commit/8a7f7ceec4d3a77a9309d935755675ac368b1eda#diff-c3bfabb5e6987aa21bc75ffd95a162d6
// As of 2016-10-16, DefinitelyTyped's defs are for shelljs v0.3.0, but we're on 0.7.0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has anyone created a PR to update this yet?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not find any updates or PRs for shelljs


/**
* Expose the internal TypeScript APIs that are used by TypeDoc
*/
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to identifying the internal APIs so we can move off of them. Way to also link to the internal TS definitions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any concern about the out-datedness that could occur here? Is there a way to build it with the internal APIs exposed instead and use that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any concern about the out-datedness that could occur

That is certainly a concern. My hope is that these internal usages can be replaced by other APIs.

@@ -4,6 +4,20 @@ import {Type, IntrinsicType} from "../../models/index";
import {Component, ConverterTypeComponent, ITypeTypeConverter} from "../components";
import {Context} from "../context";

// Copy typescript's @internal enum set from:
// https://github.com/Microsoft/TypeScript/blob/v2.0.5/src/compiler/types.ts#L2297-L2298
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason you defined this here instead of ts-internal.ts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code in ts-internal.ts exposes the existing internal/hidden APIs by augmenting the "typescript" module and exported interfaces.

In this case, the TypeFlags enum itself is exported from ts namespace. However, the Intrinsic enum value is marked as @hidden. I could not find a way to augment the existing TypeFlags enum (which is exposed as a public API) with a new enum value (which is @internal and hidden). Since I couldn't cleanly expose something from the typescript module using an augmentation, I decided to move it outside that file.

The other option, I suppose, is to export it from the ts-internal.ts file as a separate symbol (not inside the typescript namespace). I don't really have a strong opinion on this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. Unless it is needed in multiple places, defining it here is good with me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about using any to access this? My only fear is forgetting to update it if anything changes in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with the any approach.

@@ -13,32 +13,19 @@
"experimentalDecorators": true
},
"exclude": [
"./node_modules/typescript/lib/typescript.d.ts",
"./typescript/lib/typescript.d.ts"
],
"filesGlob": [
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should files/filesGlob be migrated to include?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. What do you think? I didn't change it for fear of "not knowing what I don't know".

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I honestly haven't used include yet but I think filesGlob is only supported via plugins so it would make sense to me to move to the official property with glob support. I'm curious what others think.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kill filesGlob, nothing uses it anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will kill filesGlob and the now empty exclude

@blakeembrey
Copy link
Member

Thanks a ton! It's really awesome. My only concern is with the possible out-datedness that can occur when we pull the TypeScript definition in manually, but it's an amazing start. Thanks for the PR 😄

@aciccarello
Copy link
Collaborator

My only concern is with the possible out-datedness that can occur when we pull the TypeScript definition in manually, but it's an amazing start.

@christopherthielen Did you only bring in the definitions that were used? It'd be great to try to reduce this list with future PRs (anyone interested 😉).

@christopherthielen
Copy link
Contributor Author

@aciccarello yes, the only internal APIs I added typings for are the APIs that TypeDoc is currently using.

@christopherthielen
Copy link
Contributor Author

christopherthielen commented Oct 18, 2016

My only concern is with the possible out-datedness that can occur when we pull the TypeScript definition in manually

When I created this PR, I thought the typescript.d.ts that was committed came from DefinitelyTyped. However, I now see that the definitions committed a few days ago is recent (2.0.3) and does include the private APIs. Maybe @DatenMetzgerX can explain where the definitions came from?

I did a little experimenting and found out that it's trivial to generate the full typescript.d.ts including the @internal APIs:

git clone https://github.com/Microsoft/TypeScript.git
cd TypeScript
npm i
perl -pi.bak -e 's/stripInternal: true/stripInternal: false/g' Jakefile.js
npm run build:compiler
cp built/local/typescript.d.ts ../typedoc/typescript.d.ts

I'll see if I can automate the generation of a subset of that file, including only the internal apis being used.

Potentially we could just use the full typescript.d.ts with ALL the internal APIs exposed, but I'd much prefer to limit the visibility to those APIs.

@blakeembrey
Copy link
Member

Understandable. FWIW, I think ntypescript exposes all internal APIs. You could look at how that is generated. /cc @basarat

@MichaReiser
Copy link
Contributor

@christopherthielen

It's generated as you have described. There is an UPDATING.md that describes the updating process in detail

@christopherthielen
Copy link
Contributor Author

christopherthielen commented Oct 18, 2016

@DatenMetzgerX oh, that's great to know and understand, thanks! I completely missed those instructions.

I guess the question related to this PR is then: which would ya'll prefer:

    1. continue exposing the entire internal typescript API
    1. only expose the internal APIs actually used

Option 1 is the easiest, and is what the project currently uses. We could move to @types/ for only the third party libs and use the UPDATING.md process typescript.d.ts with all internal apis exposed.

Option 2 could be automated and might be a step towards a goal of eliminating (or reducing) use of internal APIs altogether. I have this process partially automated. I suppose I'll finish automating it and add it to the PR, then ya'll can take a look and make a decision.

@felixfbecker
Copy link

felixfbecker commented Oct 18, 2016

The only question I have: will this cause "duplicate identifier" errors, if you have some of the typings already installed?

@christopherthielen
Copy link
Contributor Author

@felixfbecker this is only for building typedoc, so it shouldn't affect your project's doc generation.

@felixfbecker
Copy link

@christopherthielen I don't think so, when using typings programmatically in TypeScript it will depend on the typings.

@blakeembrey
Copy link
Member

@felixfbecker That's a real PITA. I remember having this conversation with the TypeScript team before because we can fix this on our end by providing a custom typeRoots, but I don't think typeRoots is part of the compilation output which means our configuration means nothing when we publish. /cc @mhegazy @DanielRosenwasser what's the correct approach we should take here and does/will the TypeScript definition output be updated to include the typeRoot used with the references?

@christopherthielen
Copy link
Contributor Author

christopherthielen commented Oct 18, 2016

@felixfbecker @blakeembrey I'm not sure I understand what the problem is. Can one of you clarify?

Is the problem related to using @types/ in general?

Is it about using the internal TypeScript APIs?

Or about exposing those internal APIs in ts-internal.ts as part of the TypeDoc build?

@blakeembrey
Copy link
Member

blakeembrey commented Oct 18, 2016

@christopherthielen Shouldn't affect the existing discussion, but in case you're interested it's related to #281. Basically, we should move to a system where we can publish the typings included so people can use it programmatically. However, if we declare module it'll either cause conflicts with the official TypeScript definition in a consumers project, I believe, or not be used. Either way, probably not what we want. The workaround for this in TS 2.0 is to write external module definitions (a la node_modules/@types). However, we can't do this because it's confusing and not supported - so a workaround is typeRoots in 2.0. However, this feature won't work either because the .d.ts file doesn't emit the typeRoots for a /// <reference types="typescript" /> dependency so even though we'd use it, and publish it, anyone consuming the project will fail.

For now, don't worry about it, it's just a related conversation that was somewhat relevant and it's a good example for a point I logged a while back in May (?) for TS 2.0 module support.

Edit: Note that this is only a solid issue if we expose any transitive types. We might not even do that, which makes the issue semi-mute as the workaround is just avoid it. However, it's still something that may occur unexpectedly in the future if we ever accidentally re-expose a type. A bigger issue too is that the definition we re-expose could end up different to the one we actually use (if it's compiled separately) which would be bad because the module we publish can't even be used with TypeScript.

@christopherthielen
Copy link
Contributor Author

@blakeembrey based on this information, I think we should do the following:

  1. Stop referencing internal API functions from the "typescript" module.

Instead of adding them back into the "typescript" module (like ts-internal currently does) we should simply export them from ts-internal.ts and import from there where used in the TypeDoc code.

import { getJsDocComments } from "../ts-internal";
  1. Augmenting an exported TypeScript interface should be fine. Duplicated interface merges are handled nicely by TypeScript.

For example, this works:

declare module "typescript" {
  interface Symbol {
    id?: number;
    parent?: ts.Symbol;
  }
}

declare module "typescript" {
  interface Symbol {
    parent?: ts.Symbol;
  }
}
  1. Re-declare and export the (completely hidden) @internal enum and interfaces from ts-internal
export interface CommandLineOption {
    name: string;
    type: string;
    shortName: string;
    description: DiagnosticsEnumValue;
    paramType: DiagnosticsEnumValue;
  }

Of course, we'll have to maintain them ourselves, but it seems like this approach would address both issues.

@christopherthielen
Copy link
Contributor Author

For what it's worth, I'm also affected by #281. My kludge currently looks like so: https://github.com/christopherthielen/typedoc-plugin-internal-external/blob/master/tsconfig.json#L12

@felixfbecker
Copy link

@christopherthielen What about the handlebars typings dependency? Currently it gives a "cannot find module..." when using typedoc programmatically. I assume with this PR that will go away, but what if I already have typings for handlebars installed in my project, e.g. some file that does declare module 'handlebars'? That was my question: would this PR then cause a "duplicate identifier ..." error?

@christopherthielen
Copy link
Contributor Author

@felixfbecker because the typings in this PR are listed as devDependencies, I don't think a project which depends on typedoc would be affected.

@blakeembrey
Copy link
Member

@christopherthielen That's a good point. Given that, can we make them dependencies? It'll be much more valuable if someone can actually use this module from NPM without having to manually install the dependent types.

@mhegazy
Copy link

mhegazy commented Oct 18, 2016

@felixfbecker That's a real PITA. I remember having this conversation with the TypeScript team before because we can fix this on our end by providing a custom typeRoots, but I don't think typeRoots is part of the compilation output which means our configuration means nothing when we publish. /cc @mhegazy @DanielRosenwasser what's the correct approach we should take here and does/will the TypeScript definition output be updated to include the typeRoot used with the references?

I would say the core here is not custom typeRoots, but the use of private APIs. These APIs are marked private as there are no support promises here. so they could be broken at will or removed all together.

As for typeRoots; this is mainly for users who want to override declarations of a certain package, but I do not think it should be used to publish a dependency. republishing declarations of other packages in general have proved painful for everybody. so my recommendation is to get the original package author to include them (in this case Typescript :)), or have a new package that "expands" on the original package, e.g. publish blah-internal that is not blah and then use that instead.

I left a few comments on the PR. Feel free to ignore them, i just found it easier to leave them on the code rather than listing them all on one comment.


interface Node {
// https://github.com/Microsoft/TypeScript/blob/v2.0.5/src/compiler/types.ts#L469
symbol?: ts.Symbol;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use TypeChecker.getSymbolAtLocation instead.

// https://github.com/Microsoft/TypeScript/blob/v2.0.5/src/compiler/types.ts#L469
symbol?: ts.Symbol;
// https://github.com/Microsoft/TypeScript/blob/v2.0.5/src/compiler/types.ts#L472
localSymbol?: ts.Symbol;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not think you should ever use this. it is misleading, and very subtle.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I used this to get the local name of a default export

// https://github.com/Microsoft/TypeScript/blob/v2.0.5/src/compiler/types.ts#L2166
id?: number;
// https://github.com/Microsoft/TypeScript/blob/v2.0.5/src/compiler/types.ts#L2168
parent?: ts.Symbol;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can expose this. please file an issue for it.

declare module "typescript" {
interface Symbol {
// https://github.com/Microsoft/TypeScript/blob/v2.0.5/src/compiler/types.ts#L2166
id?: number;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can expose a getter on the Symbol class to expose this as needed. please file an issue for it.

// https://github.com/Microsoft/TypeScript/blob/v2.0.5/src/compiler/types.ts#L472
localSymbol?: ts.Symbol;
// https://github.com/Microsoft/TypeScript/blob/v2.0.5/src/compiler/types.ts#L471
nextContainer?: ts.Node;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not use this. there are better ways to accomplish what you want.

*
* Faking the enum as a var (only certain codes are used by TypeDoc)
*/
var CharacterCodes: {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can expose this. though it is just ASCII codes, so you can define them yourself rely.

* Duplicating the interface definition :(
*/
interface IntrinsicType extends ts.Type {
intrinsicName: string;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use TypeChecker.typeToString(type) instead.

intrinsicName: string;
}

const optionDeclarations: CommandLineOption[];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure why this is needed. but if you have a good use case we can expose it.

function getTextOfNode(node: ts.Node, includeTrivia?: boolean): string;

// https://github.com/Microsoft/TypeScript/blob/v2.0.5/src/compiler/utilities.ts#L438
function declarationNameToString(name: ts.DeclarationName);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use TypeChecker.symbolToString instead.

function getClassExtendsHeritageClauseElement(node: ts.ClassLikeDeclaration | ts.InterfaceDeclaration);

// https://github.com/Microsoft/TypeScript/blob/v2.0.5/src/compiler/utilities.ts#L1701
function getClassImplementsHeritageClauseElements(node: ts.ClassLikeDeclaration);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nor this.

@felixfbecker
Copy link

@mhegazy What do you think about the fact that this module depends on some packages that do not have included typings, like handlebars? The idea is to make @types/handlebars a dependency so that a user can use typedoc programmatically without getting "cannot find module..." errors.

@christopherthielen
Copy link
Contributor Author

@mhegazy thanks for the feedback on the internal API usage!

@christopherthielen
Copy link
Contributor Author

I'll make another attempt at this PR in the next few days taking into account all this new information.

@mhegazy
Copy link

mhegazy commented Oct 18, 2016

@mhegazy What do you think about the fact that this module depends on some packages that do not have included typings, like handlebars? The idea is to make @types/handlebars a dependency so that a user can use typedoc programmatically without getting "cannot find module..." errors.

Let's say typedoc has a dependency with @types/handlebars (after this PR), now if you depend on typedoc and you have another declaratio for handlebars (say in typeings/handlebars/index.d.ts) you will get a duplicate declaration error.

The issue here is really how handelbars is authored, more on this later. but let's say you can not change @types\handelbars. so the recommendation to override it is to add you typings folder in the typeRoots. this way you are using your local version of the definition. e.g.:

"compilerOptions": {
    "typeRoots": [ "./typings", "./node_modules/@types" ]
}

this way the compiler will look at ./typings/handelbars/index.d.ts before looking at /node_modules/@types/handelbars/index.d.t.s.

But the main problem here is https://github.com/DefinitelyTyped/DefinitelyTyped/blob/types-2.0/handlebars/index.d.ts, it should be authored as:

...
export = Handlebars;

instead of:

...
declare module "handlebars" {
    export = Handlebars;
}

"@types/lodash": "^4.14.37",
"@types/marked": "0.0.28",
"@types/minimatch": "^2.0.29",
"@types/shelljs": "^0.3.32",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can move all these to dependencies, I'd be happy to merge today and leave the rest of the awesome comments from @mhegazy for a future PR. I'll open an issue for them too, once this PR lands.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

I'm not 100% sure this is the right thing to do though. I think we only need to add as dependencies those @types/* packages which are exposed via a public API in the typedoc.d.ts files.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@christopherthielen No, user's code won't compile if not all modules referenced in the declaration files are available.

Copy link
Contributor Author

@christopherthielen christopherthielen Oct 19, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@felixfbecker I guess that's my point, though. Only those typings that are referenced in the declaration files need to be in dependencies.

Edit: in fact, only @types/handlebars is referenced in the typedoc declaration files:

╰─ git:[masterUNSTAGED]  grep -R "///" .
./output/events.d.ts:/// <reference types="handlebars" />
./output/utils/resources/templates.d.ts:/// <reference types="handlebars" />

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's understandable. The issue with it is that you might accidentally expose one or the other at some point and forget to move them around. I think putting them in the same place as the real dependency is the best approach to avoid unfortunate side-effects.

@blakeembrey blakeembrey merged commit 603a5ff into TypeStrong:master Oct 20, 2016
@blakeembrey
Copy link
Member

@mhegazy Thanks for all your comments.

I would say the core here is not custom typeRoots, but the use of private APIs. These APIs are marked private as there are no support promises here. so they could be broken at will or removed all together.

I agree, I was trying to allude to the larger problem as it was part of a discussion we'd had in the past. As a module author, if there's an incorrect definition, I'd like to override typeRoots for development and probably publish. I don't want to have to wait days or months just to release a working version of software. This is unrelated to this module, but the bigger issue of typeRoots as a temporary solution. For reference, I've already run into this situation in production but because I use Typings and not TypeScript @types yet I had no trouble working around it (Typings can install from GitHub URLs, I just installed my fork and the PR is still waiting).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants