Open edX TypeScript Best Practices
Overview
In the past couple years, we have made a push to add TypeScript support to open edX environments, seeing the benefits of augmenting our front-end code with TypeScript’s strong typing. However, the process of migrating Javascript to TypeScript is an ongoing one, and many of us are still adapting to judiciously using it in the process of doing our normal maintenance and feature development. This guide aims to compile our best practices around migrating and using TypeScript within the open edX ecosystem.
Adding TypeScript Support
As it currently stands, not all front-end repositories have TypeScript support. To add support, it is recommended to leverage some existing libraries within the open edX ecosystem.
Using typescript-config
Repository:https://github.com/openedx/typescript-config
A tsconfig.json is required at the root of TypeScript projects, and this library provides a template with recommended compiler options and flags for using TypeScript the open edX ecosystem.
Steps:
Add
@edx/typescript-configto project’s package.jsondevDependenciesOnly if not using frontend-build: Add
typescriptlibrary and any variants of libraries with TypeScript supportCreate
tsconfig.jsonfile at the root of the projectInclude clause
"extends": "@edx/typescript-config"to pull in templateAdd references to the root directory, output directory, and directories to include/exclude in TypeScript compilation
Using frontend-build
Repository:https://github.com/openedx/frontend-build
The frontend-build package provides an out-of-the-box setup for frontend projects testing, linting, building, and development server. As of v14.0.0 it comes with TypeScript libraries.
Steps:
Include
@openedx/frontend-buildversion v14.0.0 or higher in package.jsonFollow steps from Open edX TypeScript Best Practices | Using typescript config
Update .eslintrc.js
ignorePatternsto exclude non-code .js/.json files from compilation
Examples
[frontend-app-enterprise-public-catalog] feat: add typescript support #491
Migrating Existing code to TypeScript
Basic Migration Process
Once TypeScript support is added to a project (see above), migrating a Javascript file to TypeScript is simply a matter of changing its filename extension to its TypeScript counterpart (.js → .ts, .jsx → .tsx).
However, a freshly converted Javascript file will often have TypeScript errors that will need to be addressed in order for the project to compile. See Common Issues below for examples of common failures.
Deciding what to migrate
Beyond the decision of which Javascript files to migrate to TypeScript, there is also the matter of which types, interfaces, function and variable types should be explicitly laid out. Generally it doesn’t make sense to convert and type everything all at once, and so we’ve established some recommendations here:
TypeScript Resources
Learn Typescript: Free Tutorial
type-festhttps://github.com/sindresorhus/type-fest
E.g.,
CamelCasedProperties
Generate TypeScript types via the OpenAPI schema in backend services.
Example for
frontend-app-learner-portal-enterprise
Common Issues
error TS2686: 'React' refers to a UMD global, but the current file is a module.
Make sure to import React, even if it isn't referenced in the file. This might be mostly related to test files.
error ts(2339): Property ‘x' does not exist on type xType | undefined
This can happen when trying to do a destructuring initialization off of a nested nullable member of a type/interface.
Example where this error would be seen:
type xPayload = { value: string };
type xBundle = { payload?: xPayload }; // Payload member is nullable
const bundle = { payload: { value: 'stuff' } } as xBundle;
// Error scenario
const { payload: { value } } = bundle; // 'value' would have error message: "Property 'value' does not exist on type 'xPayload | undefined'"
// Working scenario #1
const workingValue = bundle?.payload?.value;
// Working scenario #2
const innerPayload = bundle.payload as xPayload; // Specifying that bundle.payload is guaranteed to have values here
const { value: workingValue2 } = innerPayload;