RFC: Opt-in TypeScript Support for the Foreman Frontend Stack

Over the course of the last year, I have used TypeScript to write the frontend
of a Foreman plugin, and have found it to be a huge improvement, developer experience wise.
Therefore, I would like to list my findings and present a solution, which integrates TypeScript without breaking existing processes and plugins.

TL;DR: PatternFly uses TypeScript, your new Foreman plugin should too!

Advantages

  • Most obviously: Types!
    Being a statically typed language, common issues such as access of an undefined object key or calling a function on a value that is null can’t happen without the compiler complaining.
    TypeScript shifts these errors from runtime to compile-time, eliminating this entire class of runtime errors.

  • React + TypeScript = <3
    TypeScript goes very well with React and it comes as no surprise that many React libraries are written in it. PatternFly being a notable example.
    While propTypes work, they are executed at runtime, where it may already be too late. TypeScript can do their job at compile-time, which doesn’t incur the (admittedly small, but existent) performance overhead. Further, propTypes pale in comparison to actual interfaces and types, which can override and extend each other.
    Because TypeScript enjoys this deep integration into React-land, there already are extensions and integrations for all the tools we need, namely ESLint, Jest and Webpack.

  • Greatly improved IDE assistance and refactoring
    It isn’t news that editor preferences differ greatly between developers, but I use RubyMine, which has strong TypeScript support. Most of this also applies to other editors though.
    I don’t have to go back and search which fields a prop object has or manually search for uses of a component I refactored anymore. If I change the props of a component, I immediately get a list of errors telling me, where the code needs changing.
    This in particular is an issue that propTypes can’t prevent, as you’d have to open every page the refactored component is used in in a browser.

Neutral

  • foremanReact
    To work with existing JavaScript code, developers can either:

    • Write type declarations for this code or
    • Declare foremanReact as any

    While the second option is a one-line fix, I chose to go with option one and wrote declarations for everything I needed from foremanReact.
    Said declarations have to be kept in sync with the actual Foreman code, which may change over time.
    This was still worth it for me, as one major advantage of this is that my IDE can now autocomplete foremanReact imports and components.

Disadvantages

  • Learning curve
    Personally, I found the switch from JavaScript to TypeScript to be very manageable, but not everyone may see it this way.
    Therefore, the integration makes TypeScript completely optional and just one more tool in our toolbox.

The integration into Foreman

It probably isn’t news to anyone involved in frontend that Webpack is an immensely powerful tool.
It is already responsible for building the entire JavaScript frontend and takes care of .js, .jsx,
.css and .scss files.

To integrate TypeScript-based plugins, we need to tell webpack what to do with .ts and .tsx files. The rest is handled internally by webpack.

Beside the small change to the webpack config, there is no change required to existing JavaScript projects, such as Katello:

  • Webpack will check whether there are any TypeScript files,
    not find any, and proceed as usual.

  • If Webpack does find TypeScript, it will compile this code to JavaScript and bundle it.

In downstream, we have test-driven this config for the better part of a year now, and never ran into any issues when building existing plugins with it. All it does is tell Webpack to load TypeScript files using “ts-loader” and override a few tsconfig.json settings (e.g. for resolution of packages in build environments), which are needed for building Packages.

Summary

  • Integration into Foreman nightly was done in less than 100 line diff due to Webpack already providing a very strong base.

  • Usage of TypeScript remains optional

  • There are no changes required to the way Foreman packages are built and delivered.

  • No existing workflow or plugins are broken

Thanks for reading, I am very much looking forward to your responses!
Feel free to raise any questions you may have.

12 Likes

Hi everyone!

First, thanks @thorbend for the great work for for going forward with the Typescript approach. As Thorben wrote above, using Typescript is completely optional and does not break any existing workflows. I would love to go one step further and get the two PRs above in such that we get one step closer to having a fully packages foreman_ansible_director plugin.

First, we need to get the packaging merged: Add typescript loader to foreman by nadjaheitmann ¡ Pull Request #13080 ¡ theforeman/foreman-packaging ¡ GitHub

Then the other PR is able to build and the CI turns green:

If no one is strongly opposed, I’d appreciate someone from @packaging to have a look at the packaging PR.

Thanks everyone!

I was thinking “I almost got away with it.. maybe I’ll never have to learn TypeScript.. :smiley: “

kidding aside, I’m not opposed to this, especially since it’s optional and writing types has advantages even if the types are not checked (IIUC).

This may reveal how little I understand about TypeScript, but.. Can the types live in Foreman core? I wouldn’t want to have to duplicate these types in every plugin.

Thanks a lot for your comment!

Unfortunately, if the types were to live in foreman, you’d miss out on some of the convenience features. Webpack could be configured to consume those for building, but when writing code, your IDE would have no idea just what exactly TableIndexPage is.

You are entirely correct about duplication though!
The way this is handled in other projects is as follows:

Type declarations are “outsourced” to a different repository and maintained there. From there, they are then distributed as a regular npm package to anyone who needs them.

For popular projects, such as React and friends, these types live in the DefinitelyTyped repository.

In our case, you’d start by taking the few types I wrote and would then move them to a new repo called “foremanTypes” (or something along those lines) and maintain them there.

If a different project, e.g. Katello would then need them, you’d add the “foremanTypes” Package as a dev dependency.
While this is theoretically useless for JavaScript projects, a good IDE will still use those for documentation. C.f. PatternFly Button:

Since I dealt it, I would offer to maintain those types for the time being.

1 Like

I’ll preface this with saying I’m not a frontend developer and my knowledge is limited, but also that in general I think typing is a good thing. As a developer you should always think about what you’re accepting and providing. Even (or especially?) in loosely typed languages you can easily introduce bugs.

If the definitions don’t live in the same repository, they’re bound to get out of sync. I’m less worried about duplication and more that you have outdated types. That’s why I’m not a fan of introducing a foremanTypes repository. As a developer I should be able to submit a single PR to update both the function and the type definition. Having them in separate files can be OK, but coordinating 2 PRs in different repos is not a good plan.

I have worked only a little bit with Typescript to support Thorben and my experience so far is that it is surely a pain at first but it helps a lot to prevent bugs which would have been introduced otherwise. It points very clearly to the place where things mismatch and forces you to think about what you are actually trying to achieve here. Especially helpful if you don’t know the code and try to add a quick fix.

Having them in separate files can be OK, but coordinating 2 PRs in different repos is not a good plan.

I agree that this sounds like a challenge. I don’t like that plugins have to reinvent the wheel, either. However, we are at a point where AI is taking over and I made the experience that it is very good with simple but tedious tasks. Generating an API header for a specific method sounds like something an AI could do very well. So maybe one idea is to have some AI instructions to auto-generate the types. Taking this one step further, could we even have a “foremanTypes“ repository where the types are getting auto-generated whenever an interface in Foreman changes?

When I read repository, I read git repository. Is there a reason it can’t be a subdirectory within a repository? Concretely: can you have a foremanTypes directory in foreman.git that is consumed by plugins?

I also meant “Git repository”, but having this inside foreman is actually a much smarter choice, now that I think about it.
What I meant to say, was “package”. As long as the types are a seperate package, it hardly matters where they live…

There was an RFC opened to get the project as a whole to a single UI technology: UI Plan: Transitioning from PF3, ERB, and Angular to PatternFly 6

@thorbend Would you mind speaking to how you see this RFC fits into that proposal? I recognize that you call out PatternFly as being written with TypeScript. I think it would help all of us less familiar with the frontend details to understand if we still end up in a split UI technology at the end, for example.

Sorry, I missed the notification…
Yes, I have also read through that thread and strongly agree that consolidating the UI is good.
Technically, the introduction of TypeScript could clash with that idea, as it would split the UI codebase into languages.
That is, of course, not the goal of this.
Realistically, I would expect the choice of language to be decided on the “project level”, which is to say that, in the future, there might be plugins (such as foreman_ansible_director), which use TypeScript exclusively, while others continue using JavaScript exclusively.
At the maximum, I would use TS for a new major feature that is relatively decoupled from the other code, such as a new page, in an existing project. Still, even in that case, I’d probably want a quick “go ahead” from the maintainers beforehand.
To be clear, you could absolutely mix and match the two languages together on a file-by-file basis, but that doesn’t sound fun for neither maintainers, nor developers.

With that in mind, I still think having the ability to use TypeScript is good.
Ultimately, the choice of where it is used, is the maintainers’.

1 Like

I agree that we split the UI into two (but compatible) languages but the underlying UI library is still the same. If you take a look at the code, you’ll see that all the imports are the same as is you were using ‘normal‘ .js code. So effectively, we won’t have different designs as we experience now with angular and .erb files. We use the same components for JS and TS. So style-wise, there is no difference if you write the UI with JS or TS.

However, there is some overhead from the packaging side as typescript needs the type definition for each library and those need to be packaged separately (for libraries that are not written in TS). Typescript also needs a specific loader.

So it boils down to having the same core libraries with corresponding TS additions while the resulting UI style is identical.

1 Like