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.
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.
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.
I was thinking âI almost got away with it.. maybe Iâll never have to learn TypeScript.. â
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.
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.
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:
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âŚ
@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â.
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.