Proposal: Native TypeScript support in Gutenberg

What is TypeScript?

Simply put, and taken from the TypeScript website:

TypeScript extends JavaScriptJavaScript JavaScript or JS is an object-oriented computer programming language commonly used to create interactive effects within web browsers. WordPress makes extensive use of JS for a better user experience. While PHP is executed on the server, JS executes within a user’s browser. https://www.javascript.com/. by adding types.

You are able to add types by using the TypeScript language itself, which is a superset of JavaScript. It resembles JavaScript in every way except for the addition of types. Alternatively, you can add types through JSDoc annotations. GutenbergGutenberg The Gutenberg project is the new Editor Interface for WordPress. The editor improves the process and experience of creating new content, making writing rich content much simpler. It uses ‘blocks’ to add richness rather than shortcodes, custom HTML etc. https://wordpress.org/gutenberg/ so far has opted for the latter.

Type checking is also coming to PHP and many other dynamic languages, so increasingly type checking is becoming less of a foreign concept for fans of dynamic languages.

What is being proposed?

This post proposes that we transition away from JSDoc annotations and towards the TypeScript language itself. This is proposed because it unlocks powerful features of TypeScript that are unavailable through JSDoc annotations and will encourage new contributions by community members in this space by lowering the learning curve and leveraging existing knowledge of statically typed languages.

Essentially this means beginning to use .ts(x) files in addition to existing .js files. I emphasize “in addition to” because I want to stress that I am not suggesting that we eliminate traditional JavaScript from Gutenberg, nor is the proposal to fully re-write Gutenberg in TypeScript. The fact is that the majority of Gutenberg will probably forever remain as plain old JavaScript. However, I think in our coreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress. libraries (data, compose, components, dom, etc) we should transition to a heavier use of type-checking and the TypeScript language itself to ease that transition.

A brief history of TypeScript in Gutenberg

In August of 2019, TypeScript was first introduced for type checking modules through JSDoc. Then, a few months later in November, type validation for modules was officially proposed:

While Gutenberg packages are not authored in TypeScript, we can still benefit from its JavaScript type checking using the JSDoc we already write. This will bring us some benefit of type safety, even as we continue to write modules with JavaScript, not TypeScript.

Andrew Duthie

Documentation explaining how to use JSDoc with JavaScript was added in December of the same year. The current version of that documentation lives here. In March of 2020 we began using TypeScript to output package type declarations as a way of progressively replacing the DefinitetlyTyped @types packages for the @wordpress scope of packages. Documentation was later added explaining how to type pack

ages with TypeScript and JSDoc. Finally, in February of this year, the react-i18n package was added which introduced the first package fully written in native TypeScript.

How we do things today

We use TypeScript to generate .d.ts files for certain packages in Gutenberg based on JSDoc type annotations. This allows us to “sprinkle” TypeScript support throughout our normal JavaScript codebase without having to do re-writes of existing modules in TypeScript. In addition to generation the TypeScript compiler also checks our code for type safety. For example, you type the a function like this:

/**
 * @param {number} x
 * @param {number} y
 * @return {number}
 **/
function add( x, y ) {
    return x + y;
}

And try to pass in a string to this function, you’ll get a compiler error. This is enforced through a pre-commit hook as well as the static-analysis GitHubGitHub GitHub is a website that offers online implementation of git repositories that can easily be shared, copied and modified by other developers. Public repositories are free to host, private repositories require a paid subscription. GitHub introduced the concept of the ‘pull request’ where code changes done in branches by contributors can be reviewed and discussed before being merged be the repository owner. https://github.com/ action that is run on all PRs.

Problems with the JSDoc approach

When we adopted JSDoc annotations as our approach to adding types in Gutenberg, we hoped we could improve the experience for new contributors by providing great auto-complete and quality assurance help in editors like VS Code while not requiring them to actually learn TypeScript. However, adopting the JSDoc syntax actually has significant downsides. First, few TypeScript examples use JSDoc. When a new contributor wants to express a moderately complex type, they find themselves translating between TypeScript example they find online and the less common, JSDoc version. Second, there are some features of TypeScript that are completely unavailable in the JSDoc variant of the syntax. And those features, like template parameter defaults, can potentially provide a lot of help for developers trying to use Gutenberg’s components or the @wordpress/data package.

Surprise! We already use TypeScript (compiler)

By now you may have thought to yourself, “Wait, it sounds like we already use TypeScript?” The secret is that yes, we already use TypeScript in Gutenberg… just with the a poorly documented, less expressive syntax (JSDoc). For packages that support it (those that already emit their own .d.ts files) the TypeScript compiler is already being used for type checking, as stated above with the addition example. Likewise, when reading JSDoc typed JavaScript code, one doesn’t need to just pay attention to the code they are immediately reading, but also displaced type definitions (the type definitions live separately from the code itself). This means that the effective difference between what exists today and what is being proposed in this post is a (massive) syntax improvement as well as the ability to unlock TypeScript’s full potential by being able to expressively type core libraries like compose and data.

While this is admittedly a new syntax and may seem scary, I would urge you to consider the difference between these two blocks of code:

/**
 * @param {object} props Props
 * @param {string} [props.className] Classname
 * @param {(value: string) => void} [props.onChange] Change handler
 * @param {import('react').Ref<HTMLInputElement>
 */
function CustomInput( props, forwardedRef ) {
  // ...
}

Versus:

interface Props {
  className?: string;
  onChange?: ( value: string ) => void;
}

function CustomInput( props: Props, forwardedRef: Ref<HTMLInputElement> ) {
  // ...
}

This represents the majority of the changes that would occur if we adopted TypeScript in places where we currently use JSDoc.

I don’t want to discount the learning curve of a new language—TypeScript definitely has its own learning curve. However, I think that learning curve is worth the benefits elucidated in the following section. Likewise, regular JavaScript with JSDoc annotations will continue to be available as an option and for many use cases it will continue to be a good option (like adding types to existing code where the types themselves are straightforward).

For a while, this syntax improvement was not possible because we lacked tooling. For example, we didn’t have ESLint support and docgen was unable to process TypeScript files. Both of those problems have since been addressed in these PRs:

ESLint support: https://github.com/WordPress/gutenberg/pull/27143

docgen support: https://github.com/WordPress/gutenberg/pull/29189

Now that these tooling issues are mostly behind us, I think we’re ready to move forward with native TypeScript in Gutenberg.

Benefits of native TypeScript

By allowing native TypeScript support in the Gutenberg repository, we continue to leverage the existing benefits we’ve already bought into by using the TypeScript compiler to generate .d.ts files (remember that this does not merely extract the types from the JSDoc comments but also checks the soundness of the code itself) with the vastly improved syntax of native TypeScript. It’ll also allow us to leverage the existing expertise of members of our community who are familiar with TypeScript (and other typed languages) rather than having to re-learn how to write types using the far-less than ideal JSDoc support.

Native TypeScript will also unlock the full power of the TypeScript type system. Currently there are a few edge cases that are explicitly not supported by the JSDoc annotations. For example, it’s not possible to create optional type parameters. The following is impossible to express in JSDoc:

type GenericType<T = any> = { foo: T };

Furthermore, native TypeScript will allow us to type complex core systems (like @wordpress/data and @wordpress/compose) without adding massive amounts of “noise” to each file in the form of complex, difficult to read, and even harder to maintain JSDoc type annotatons. For example, typing createHigherOrderComponent in native TypeScript is a breeze relative to the complex expressions you run into trying to type it using JSDoc annotations. Typing these core systems gives us both the guarantee of their soundness as we make improvements and grants community contributors using those libraries the same guarantee that they’re using the APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways. in a sound way.

Where should this change happen and who will it effect?

First, I want to emphasize that this changes basically nothing for custom blockBlock Block is the abstract term used to describe units of markup that, composed together, form the content or layout of a webpage using the WordPress editor. The idea combines concepts of what in the past may have achieved with shortcodes, custom HTML, and embed discovery into a single consistent API and user experience. authors. While @wordpress/babel-preset-default includes TypeScript transpilation support, create-block has not been updated to support TypeScript. Likewise, the dependencies for block authors, like the @wordpress/block-editor package, do not have up-to-date type definitions (it has defintions on DefinitelyTyped but these have probably fallen out of sync with the code itself) or do not have type definitions at all (meaning the TypeScript compiler will complain about not being able to understand your dependencies). TypeScript for block authors will likely take some significant community support to enable type generation, whether through native TypeScript or JSDoc type annotations, in higher-level packages.

That being said, let’s take a look at who is already affected by TypeScript in the Gutenberg code base. One third of Gutenberg packages are currently type checked (meaning they have a tsconfig.json), either fully or partially and the pre-commit hook won’t let you commit and JSJS JavaScript, a web scripting language typically executed in the browser. Often used for advanced user interfaces and behaviors. code there that wouldn’t be approved by the TypeScript compiler. To make any contribution to this growing list of packages, you must already know the TypeScript type system in order to understand type checking errors as well as to understand how to type any new code that you add. This has been the status quo for many months now (see the history section above).

Taking a closer look at the packages that currently garner the most contributions per year, here are the packages that have attracted more than 100 commits in the last year:

packagecommitstsconfig@types
block-library1093x
block-editor950x (partial)x
components626x (partial)x
e2e-tests454
edit-site292
editor257x
edit-post246x
edit-widgets163
reactReact React is a JavaScript library that makes it easy to reason about, construct, and maintain stateless and stateful user interfaces. https://reactjs.org/.-native-editor152
edit-navigation135
scripts116
blocks114
block-directory107
interface102

The tsconfig and @types columns denote whether the package is type checked or has DefinitelyTyped type defintions respectively. At the moment, none of these 14 packages publish type definitions natively and community contributors who use TypeScript for their projects must rely on DefinitelyTyped when it is available.

The block-library package type checks 2 specific files and ignores the rest (for now, hopefully this will be expanded in the future).

This shows that for the vast majority of contributions, TypeScript support is irrelevant. Now, that doesn’t meant that TypeScript support isn’t coming to those packages. The main roadblock to it, however, are lower level packages that are currently untyped like compose and data.

components is a bit of an outlier in that it is a widely contributed-to package that has a lot of it typed. Indeed there is an ongoing effort to type as much of components as possible, due to its wide-spread nature. It is believed that the best way to show TypeScript’s power is to have a widely used library like components typed. However, it is roadblocked by compose and other core libraries not being fully typed.

Those low level packages garner far fewer contributions by a much smaller group of people than the 14 packages listed in the table above. compose, for example, received under 60 significant (non publish, non dependency update, and non documentation update) commits over the last year from a group of less than 10 people. By progressively and carefully re-writing parts of compose in native TypeScript, we can have a ripple effect where the benefits of TypeScript will by felt throughout the Gutenberg codebase without actually affecting how most contributors write their code. Most contributors consume compose in their own code but do not contribute directly to it.

In summary, there are a few packages like compose, element, i18n, and data that share the following traits:

  • they are foundational infrastructure packages relied on by everything else
  • they don’t attract mainstream contributor interest. They are worked on mainly by “experts”
  • they are more likely to use complicated types with generics and other tricks. Functions from compose are a crystal clear example of that

Switching these to native TypeScript would have a big impact: solid type-safe foundational libraries. And the downside is limited: most contributors don’t come into much contact with these.

The path forward

In packages that support JSDoc type annotations, you can already swap out JavaScript for TypeScript without any concern as Babel is already ready to consume TypeScript (it simply strips the type annotations from the files). For example, if you wanted to re-write and any file that is already JSDoc annotated in TypeScript, you could do that today without any problems (really, if you don’t believe me try it!). There may be some caveats like needing to explicitly annotate return types of functions so that docgen can consume them, but these are easy to enforce, beneficial to the health of the code in general, and also not different than the limitations that currently exist with JSDoc—docgen will give you a helpful error if it cannot find the return type of a function you’ve asked it to document and explicit annotation of inputs and outputs is a good thing and we already do it through the @return tagtag A directory in Subversion. WordPress uses tags to store a single snapshot of a version (3.6, 3.6.1, etc.), the common convention of tags in version control systems. (Not to be confused with post tags.).

For packages that are currently not type fully annotated (like compose, dom, and data) we can begin to progressively re-write the more complex, core parts of it in TypeScript. We don’t have to re-write everything, nor should we spend our valuable time merely re-writing files that don’t need it. However, in the cases where native TypeScript allows us to more fluidly, confidently, simply, and beautifully express the types of a module, we should do so (compose and data are tremendous examples of this).

We’ve already seen some of the benefits of using TypeScript JSDoc syntax in Gutenberg. Our documentation has improved, and everyone from Gutenberg contributors to block developers to site builders enjoys better auto-complete and error handling in their editors. When we use the better supported, more expressive standard TypeScript syntax, we’ll only improve that experience for everyone involved. If you want to contribute to Gutenberg, don’t be intimidated by the new syntax! TypeScript, much like WordPress, enjoys a rich, supportive community with many quality examples and tutorials.

Acknowledgements

Thanks to @gziolo, @griffbrad, @nerrad, @youknowriad, @jsnajdr and many others for reviewing and/or contributing to this proposal.