Cesium Chinese Website: http://cesiumcn.org/ | China Fast Access: http://cesium.coinidea.com/
With the current 1.70 release, CesiumJS now ships with official TypeScript type definitions!
TypeScript definitions have been a long-requested feature. While the community has done work supporting various manual approaches, the most popular being @types/cesium, the sheer size and evolving nature of the Cesium codebase made manual maintenance an endless task. The official definition file Cesium.d.ts is over 42,000 lines and 1.9MB in size.
Even if you’re not a TypeScript user, this work has improved the correctness and completeness of the CesiumJS API reference documentation and enables better IntelliSense support in IDEs that can apply TypeScript definitions for type inference, making it a huge win for the entire CesiumJS community.
Updating CesiumJS to 1.70 will automatically leverage type checking in TypeScript applications. We use the types field in package.json, which requires no additional configuration in most cases. However, if you import individual Cesium source files directly, you’ll need to add “types”: [“cesium”] to your tsconfig.json to pick up the definitions. If you were previously using @types/cesium, you can remove it.
Official support from the CesiumJS team means the latest and correct definition file will ship with every release. It also means TypeScript support will be officially tracked as part of the CesiumJS GitHub repository. If you find a bug when using CesiumJS with TypeScript, please open an issue or better yet, a pull request to fix it. If you have questions about CesiumJS/TypeScript or need help debugging your project, please ask on the community forum.
If you’re using custom or @types/cesium definitions and aren’t ready to switch, you can delete Source/cesium.d.ts after installation. TypeScript tooling will then fall back to the next set of CesiumJS type definitions it finds.
The official type definition file, Cesium.d.ts, documents over 42,000 lines of declarations and documentation, weighing in at 1.9MB.
Deep Dive
While we’re excited to finally officially support TypeScript, it took some effort to get here. Initially, we explored 3 options:
Manually Maintaining the Definition File
We could manually manage and maintain our own TypeScript definition file as part of the CesiumJS codebase, most likely with a separate definition file for each JavaScript file, making it easier to manage – for example, Cartesian3.js would have a corresponding Cartesian3.d.ts. This would be technically easy to implement, but would cause significant harm in terms of file synchronization and maintainability.
Additionally, we didn’t want to include only declaration interfaces without inline documentation, so users could take full advantage of IntelliSense. This was our last resort, but if it turned out to be the only viable option, it would be our final choice.
Porting CesiumJS to TypeScript
You may be surprised to hear that we actually evaluated rewriting all of CesiumJS in TypeScript. For TypeScript developers, this would be a huge improvement, and for CesiumJS maintainers and the codebase, it would be a real win. Beyond strong type checking, it would allow us to quickly adopt modern conventions like template literals, arrow functions, and async/await, which we currently don’t allow in the CesiumJS codebase due to compatibility and tooling reasons.
Unfortunately, the level of effort and volume of work required made it an unattractive option in the short term. This option is still on the table, but like the large-scale ES6 migration we did last year, it would require significant careful planning, research, and infrastructure work to execute properly.
Generating Definition Files with the TypeScript Compiler
Starting with TypeScript 3.7, the compiler can compile JavaScript code with JSDoc annotations and generate corresponding type definition files for us. This approach completely eliminates the need for manual .d.ts file maintenance and has the additional benefit of validating and improving our own JSDoc annotations, since they need to be accurate to generate correct type definitions. Needless to say, this option was very attractive to us, and we decided to pursue it after some preliminary prototyping experiments showed it could work.
In practice, we spent several weeks working through this approach. Marco Hutter was heavily involved in documentation fixes and source code adjustments to make the compiler happy. Early work was promising. As expected, it exposed errors and inconsistencies in JSDoc annotations and, to a lesser extent, in the CesiumJS API that we fixed. Unfortunately, we soon hit a wall.
Relying on the TypeScript compiler meant we lacked options when it did something incorrect or unexpected. While the compiler uses JSDoc annotations in some cases, in many cases it relies on its own type inference and provides no way for us to override it. It also completely ignores much of the JSDoc, such as on object-defined properties, and exposes all private underscore variables as part of the definitions. This led us to bend the CesiumJS codebase in ways we were uncomfortable with, just to make the TypeScript compiler happy. We considered trying to modify the TypeScript compiler itself, but we would have had to dive deep into the compiler code, and we weren’t sure what the maintainers would accept or how long the process would take. Ultimately, our favorite solution became a drawn-out gamble, and we lost confidence in the approach.
Left: JSDoc for UrlTemplateImageryProvider that unexpectedly adds properties to BingMapsImageryProvider; Right: The generated BingMapsImageryProvider definition with duplicate definitions, causing compilation failure.
Back to the Drawing Board
As it turned out, we were so excited about TypeScript’s official JSDoc support that we completely overlooked a similar option: tsd-jsdoc. tsd-jsdoc is a JSDoc plugin that generates TypeScript definitions from JSDoc output. This makes it very similar to the TypeScript compiler approach, but provides much more flexibility over the generated type definitions.
tsd-jsdoc doesn’t directly parse JavaScript but instead relies on the abstract syntax tree (AST) generated by JSDoc. This means it’s not affected by type inference issues or lack of JSDoc completeness that nearly derailed the TypeScript compiler approach. If we can express a type using JSDoc annotations, then it appears in the type definition file as we want it.
We had already learned a lot from our earlier TypeScript compiler approach failures, so we were able to complete a feasibility evaluation quite quickly, and all the issues with our existing incorrect JSDoc still applied. Things progressed faster than we expected, and we knew we had found the solution.
As developers, sometimes we get so focused on a particular technology that we overlook other options. In this case, community member @bampakoa had even submitted pull requests to both CesiumJS and tsd-jsdoc last year to make them more compatible. We already knew tsd-jsdoc existed, but we had overlooked it in our initial evaluation because we assumed the TypeScript compiler option would be better, and we inadvertently developed a blind spot for tsd-jsdoc.
Post-Processing and Validation
While the tsd-jsdoc output is fairly high quality out of the box, we did some additional post-processing to further improve it. This includes simple string manipulation, regex find-and-replace, and even using the TypeScript compiler to rewrite parts of the file. All of this happens as part of the new build-ts gulp task. If you’re curious, you can check out the code. The final result is a single Cesium.d.ts that serves as the entry point for the generated Cesium.js module.
In addition to generating the output, the build-ts task also validates the file by compiling it with TypeScript. If a developer makes a mistake in JSDoc, such as misspelling a class name or referencing a private or non-existent type, the build process will fail. While this validation process is very useful, it only catches certain types of errors. For example, if someone implements a new ImageryProvider that doesn’t conform to the correct interface, the definition file will compile without errors, but TypeScript will emit a compilation error in applications that try to use the new class as an ImageryProvider.
We’re still exploring ideas for adding additional validation, such as writing some unit tests in TypeScript to identify potential problem areas during development.
JSDoc Errors
I’ve mentioned several times that a particularly exciting aspect of the JSDoc-based approach is that it adds another level of inspection and validation to our documentation, benefiting everyone, not just TypeScript developers. A major part of our documentation review process is now automated. The issues we found in the codebase can be categorized as follows:
- Incorrect or incomplete types - In many cases, we used informal or incorrect names for types, for example, Image instead of HTMLImageElement, Canvas instead of HTMLCanvasElement. An interesting example is TypedArray, which doesn’t even exist at the specification level but is a common term for the full list of types like Int8Array, Float32Array, etc. We also had incomplete generics, such as Promise instead of Promise
. - @exports - We used JSDoc’s @exports tag as a final fallback. If a developer couldn’t get something to show up in the generated HTML, they might add @exports, which would “just work.” We used @exports for enums, namespaces, and functions instead of the @enum, @namespace, and @function tags. This led to incorrect type generation. In practice, we don’t need @exports anywhere in the code.
- Private type leaks - Many private types were referenced in the public API. These private types don’t exist in the HTML output and are just silent failures for our documentation build step. In most cases, it made sense to simply make the private types public. Thankfully, we also have a habit of documenting private types in CesiumJS, so we didn’t have to write new JSDoc.
- Copy-paste errors - The last category of JSDoc errors was duplicate parameter entries related to copy-and-paste, such as having ImageryProvider A declare that it’s documenting properties on ImageryProvider B, etc.
Next Steps
Once the community starts using these definitions, we expect some minor issues to surface over the next few CesiumJS releases. We’ve also started developing a list of ideas we want to explore, such as leveraging generics for the property interfaces used by the Entity API. Ultimately, we rely on the community to tell us what matters most to developers so we can shape our CesiumJS TypeScript roadmap.
We also want to figure out a way to use the TypeScript definitions within the CesiumJS codebase itself. We believe VSCode has some mechanisms for this, but we haven’t explored them yet. If this proves feasible, it would be a major win and would enable another level of validation through plain JavaScript, not to mention making developing CesiumJS an even better experience than it already is.
I’m sure many people perked up when I mentioned that we evaluated rewriting CesiumJS in TypeScript. I’m absolutely in favor of this in the long run. As part of the evaluation process, I actually used the TypeScript compiler to build the existing JavaScript codebase and even ported some basic files (like Cartesian3.js) to TypeScript to understand how to do TS/JS hybrid development, rather than an “all-at-once” migration strategy. Much like ES6, porting the code is the easy part. Expect a GitHub issue soon that will break down everything that needs to happen to make a TypeScript version of CesiumJS a reality; but there are no commitments yet.
Acknowledgements
I just want to thank the community once again for helping generate ideas and discussions about TypeScript over the past few years. Special thanks to @thw0rted, who was the first external contributor to improve the initial TypeScript type definitions and provided a lot of great feedback in the original pull request. Finally, a huge thanks to my partner and co-maintainer Kevin Ring, who not only provided extensive expert knowledge and feedback but also dove into the work himself, ultimately making a series of improvements to the code.
Author: Matthew Amato
Original link: https://cesium.com/blog/2020/06/01/cesiumjs-tsd/
Comment: The introduction of TypeScript makes CesiumJS a more professional library and easier to maintain. Of course, the migration process is painful.
Cesium Chinese Website QQ Group: 807482793
Cesium Chinese Website: http://cesiumcn.org/ | China Fast Access: http://cesium.coinidea.com/
