Using TypeScript with WordPress Gutenberg Blocks

  • infoFull Post Details
    info_outlineClick for Full Post Details
    Date Posted:
    Apr. 17, 2021
    Last Updated:
    Apr. 17, 2021
  • classTags
    classClick for Tags

Intro

For those unfamiliar, Gutenberg Blocks, or now known simply as Blocks, are the backbone behind the new reusable component-driven paradigm for WordPress.

For more info on Gutenberg, you can check out WordPress’s landing page.

Now, if you are reading this, there is a good chance that you are a WordPress developer, maybe even already familiar with Blocks and how they work. There are lots of dev guides out there on how to use Blocks (including my own notes, here), but I was recently having some issues using Blocks with the TypeScript programming language (which gets converted into JavaScript) and found much less out there to guide my way.

This post covers some steps I had to take to get TypeScript working with WP Gutenberg Blocks, as well as some issues that I ran into.

Steps

The first step, whether or not you end up using TypeScript, I would recommend to be to install and start using wp-scripts (source code, docs) for orchestrating your build setup. You don’t have to do this, but it makes things a lot easier while still being extensible via configuration files and CLI arguments.

Now, assuming that you are using the wp-scripts, or something similar, here are the general steps I had to take to get TypeScript working

  • Install TypeScript and ts-loader locally
    • npm i -D ts-loader
    • npm i -D typescript
  • Create a tsconfig.json file and adjust it (see below)
  • Modify the WebPack config to use ts-loader (see below)
  • Install types for packages that distribute them separately (i.e. through the @types monorepo)
  • Ambient type-declare for packages that are missing them (see below)

TSConfig

Since your WebPack config will override a lot of settings of your TSConfig, there is less to worry about, but still some settings that really matter.

{
    "compilerOptions": {
        // Note: Webpack config overrides this anyways
        "outDir": "./dist",
        // React compatibility
        "jsx": "react-jsx",
        // We want ESM supported in the output, because WebPack is going to bundle this anyways,
        // and we want the tree-shaking support and some other benefits
        "module": "ESNext",
        "target": "ES5",
        // == Now for some settings that are not *strictly* required, but I recommend:
        // Embrace strong-typing!
        "strict": true,
        // Not strictly necessary, but nice
        "allowSyntheticDefaultImports": true,
        // Recommended
        "moduleResolution": "node",
    }
}

Modifying the WebPack Config

📄 WebPack has a guide on using TypeScript with WebPack.

For actually generating our final code that will be distributed, we are still going to be using WebPack, not the TypeScript compiler (tsc). However, we have to tell WebPack about the TypeScript code we have added and give it a tool to load it – that is why we added ts-loader as a devDependency.

Luckily for us, wp-scripts not only allows us to use a custom WebPack config, but also exports the base config so that we can extend it instead of replacing it.

webpack.config.js

// This comes from https://github.com/WordPress/gutenberg/blob/trunk/packages/scripts/config/webpack.config.js
const wpConfig = require( '@wordpress/scripts/config/webpack.config' );

// Modified config
module.exports =  {
    // Since whatever object we export from this file will *replace* the default config, we need to
    // make sure to constantly pull in the default values from WordPress's config file
    ...wpConfig,

    // Point to your main file, wherever it is, which should now be TS
    entry: `./src/index.ts`,

    // We need to add a new rule to process `.ts` and `.tsx` files with `ts-loader
    module: {
        ...defaultConfig.module,
        rules: [
            ...defaultConfig.module.rules,
            {
                // Notice that this regex matches both `.ts` and `.tsx`
                test: /\.tsx?$/,
                use: [
                    {
                        loader: 'ts-loader',
                        options: {
                            // You can specify any custom config
                            configFile: 'tsconfig.json',

                            // See note under "issues" for details
                            // Speeds up by skipping type-checking. You can still use TSC for that.
                            transpileOnly: true,
                        },
                    },
                ],
            },
        ],
    },
    // This merges the extensions setting with whatever WP has in their config, but also adds TS
    // extensions, and puts them first, to indicate preferred resolving over .js files
    // @see https://webpack.js.org/configuration/resolve/
    resolve: {
        extensions: [ '.ts', '.tsx', ...(defaultConfig.resolve ? defaultConfig.resolve.extensions || ['.js', '.jsx'] : [])],
    },
}

Issues

Other than getting the configuration setup, there were a few issues that I ran into that I feel would be helpful to share.

Untyped packages

  • Some WordPress packages offer type definitions (either bundled directly, or, very frequently, distributed via @types, such as @types/wordpress__blocks). However, there are still a bunch that are completely “untyped”, which will throw all kinds of errors in TypeScript, especially if you have strict rules on.
  • To address this, you either need to wait for types to be added upstream, do it yourself and make a PR to merge them (help the open-source community!), or, if you are in a rush, you can augment the types by using ambient declaration files.

TS-Loader: Refuses to Stop Type-Checking node_modules

One frustrating issue that I ran into was that I simply could not get my build command to stop type-checking node_modules – this is a big deal because A) it slows everything down, and B) it actually finds errors and will throw an exception. But it’s not my code!

Trying all the common settings had no effect, such as explicitly excluding node_modules through tsconfig, excluding through the WebPack config, and passing explicit inputs.

However, I did eventually find the culprit, as well as two solutions.

The culprit for me was that I was using "checkJs": true in my tsconfig.json file, which apparently causes ts-loader to ignore the exclude rules and start checking node_modules. Although this issue is marked “Closed” on Github, I can assure you it still exists (even with TypeScript version 4.2+).

Solutions:

  • Recommendation: Run ts-loader with the transpileOnly setting changed to true.
    • This stops WebPack from type-checking your code entirely, which means that you should run tsc separately with tsc --noEmit when you want to type-check
    • This also speeds up build and reload times!
  • You can create a separate TS config file just to use with ts-loader, such as tsconfig.webpack.json, which has "checkJs": false

Using TypeScript Changed The Output Filenames

The last interesting issue that I encountered was that changing the entry point of my WebPack config to point to a TypeScript file changed all the output filenames from index.* to main.*. I’m not sure exactly what caused this internally (my guess would be something hidden in the WordPress Webpack base config that handles chunk names), but the fix was easy enough.

Just make sure that in the PHP side, when you register and enqueue assets for your block, you use the updated filenames that show up in the build output.

If you really want to keep the original filenames, you can override the output settings in the WebPack config: see how this sample repo solved it.


If you are looking for more resources on TypeScript + Blocks, there aren’t many guides out there, but this blog post by Kazmierczak is a nice succinct explanation, and they also have a sample repo.


Feel free to let me know if I’ve missed anything or you have tips of your own to share! Hope this helps someone!

Leave a Reply

Your email address will not be published. Required fields are marked *