TypeScript at Lyft – Lyft Engineering


In my early days as a JavaScript developer, when I learned about efforts to add types to JavaScript projects, the first question I asked myself was: why?

Now, as a seasoned JavaScript veteran, I cannot imagine writing JavaScript without support of a type system. Large JavaScript applications need type information for scalability and maintainability. Our numerous JavaScript projects at Lyft are no exception, from our Lyft.com website to our many internal tools. While I am still an ardent JavaScript fan, watching Lyft’s teams and codebase grow has convinced me that vanilla JavaScript was never intended for use in large-scale applications.

But that’s no reason to throw out JavaScript altogether. Type systems can make all the difference. It’s no secret that type systems can avoid bugs and help engineers more easily navigate codebases. Let’s consider how — and why — Lyft chose TypeScript for the job.

Bugs!

Uncaught TypeError: Cannot read property "foo" of undefined is one of the most common JavaScript errors. It occurs when accessing properties of a reference that could possibly be undefined. For our vanilla JavaScript codebases, some form of this error frequently cropped up in Lyft’s production environment. Other category of bugs of this sort include simple typos; something like document.getElementbyId can cause big problems in production.

Less frequent — but more important — bugs are REST API type mismatches between the client and the server. For instance, when a field is changed from number type to string in the API response, the JavaScript code can result in very unexpected behaviors.

Developer Productivity

Navigating large JavaScript codebases can be time consuming and confusing. It’s often hard to find where functions are defined and what parameters they expect. Take for instance the following JavaScript code:

This function makes an HTMLInputElement, and depending on the type, it sets the checkboxes checked attribute to the (supposedly) booleanvalue argument. Otherwise it will assign thevalue argument to the value attribute of the input.

There’s a possible, subtle bug here. Consider what happens if you assign the wrong type of value when making a checkbox input:

Even with an accurate JSDoc comment, this bug is not prevented. Setting the checked value argument to the string "false" will actually make createInput return an input in checked state — because the string "false" is evaluated as a boolean true!

With a type system we can declare function overloads that help developers write correct code from the get-go:

Type systems make discovering code capabilities much easier. They also prevent bugs very early on by hinting at API usages semantics.

Type systems make refactoring easier by giving developers confidence about their changes. For example, when a utility function signature changes, the TypeScript compiler will not allow compilation until all call-sites are corrected with the new signature.

Moreover, strongly typed programs are easier to refactor because the type-checker ensures your changes are compatible with other parts of the project. IDEs and code editors can provide refactoring features backed by the type system.

Type systems also help with the maintainability by distributing modules with type information. Modules written in TypeScript can enable some editors to provide very helpful API usage hints.

TypeScript vs. FlowType

Once we were convinced that a type system would help scale our frontend applications, we considered our options. There are a few type systems available for JavaScript:

  • Google Closure Compiler with JSDoc type annotations
  • FlowType
  • TypeScript

We ultimately decided to use TypeScript, but it wasn’t an easy decision. Our team was evenly divided between those who preferred FlowType versus those who preferred TypeScript.

There were many arguments on both sides, and it’s helpful to consider them in detail.

“FlowType is Just JavaScript”

FlowType is sometimes advertised as “just JavaScript” or “JavaScript with type annotations.” This statement is very misleading. FlowType is an independent language, with syntax that’s a superset of JavaScript. The convention of using .js extensions for FlowType files contributes to this confusion. But in reality, neither JSX or FlowType are JavaScript. The same goes for TypeScript and even EcmaScript proposals that are not yet in the spec.

Neither FlowType nor TypeScript are JavaScript; they are different languages.

Call-Site Type Checking

One of much-hailed features of FlowType is call-site based type checking for function calls. Consider this erroneous code:

FlowType will — correctly — warn about passing a string to a function that clearly requires a number as its sole argument. TypeScript will consider a to be any type and compile the above code without errors.

This feature looks impressive but it falls short as soon as we complicate our function even slightly:

This code will get compiled in FlowType without any errors. Call-site type checking is impressive but casts a false sense of security when burdened with the codebase of real-world applications.

React

Since React and FlowType are both open source projects from Facebook, it’s often assumed that FlowType works better than TypeScript with React. But with projects at Lyft, we didn’t find any meaningful difference when using either with React. React type definitions in DefinitelyTyped are very accurate and helpful.

Popularity

Even if you consider both options to be equally suitable, for future ecosystem growth it’s critical to consider a project’s popularity. Using a popular option will help Lyft attract more talent while ensuring broader access to other open source projects written in that language.

Though measuring the popularity of an open source project can be a subjective art, I tried my best to find reasonable metrics as a basis for comparison:

StackOverflow questions: FlowType: ~900; TypeScript~38,000

GitHub Issues: FlowType: ~1,500 Open, 2,200 Closed; TypeScript: ~2,400 Open, 11,200 Closed

GitHub pull requests FlowType: ~60 Open, 1,200 Closed; TypeScript: ~100 Open, ~5,000 Closed

npm download per month FlowType: ~2.9 million/month, TypeScript: ~7.2 million/month

Number of external type definitions

FlowType: ~340 external, 43k for “flow-typed” directory in GitHub; Some libraries provide .flow type definition

TypeScript: ~3,700 external, ~250k results for “typings” in package.json in GitHub, Even FB repos like Redux and ImmutableJS provide TS type definitions

We even sent out a survey to our own engineers to investigate internal popularity of each project.. Mirroring the external numbers I collected, we discovered that TypeScript was also more popular internally.

Migrating to TypeScript

We have a daunting amount of preexisting vanilla JavaScript code. Converting all of our JavaScript codebase to TypeScript was not an option.

So instead, we took the incremental approach. We are using Webpack to compile our frontend applications. It’s painless to introduce TypeScript to Webpack via TypeScript-loader. Once TypeScript-loader is added to the Webpack configuration of a project, we were able to write new files in TypeScript and update our codebase piecemeal.

The TypeScript compiler can type-check JavaScript files as well. We took advantage of this feature to check our existing JavaScript code for type errors. This only works if a JavaScript file is directly imported into TypeScript. In the most recent version (2.5) it’s also possible to type-check standalone JavaScript files.

Teaching TypeScript

There are many resources online for learning TypeScript. The TypeScript website itself includes great learning material, making onboarding engineers a snap. Moreover, since TypeScript syntax is just a superset of JavaScript, it’s very intuitive for frontend engineers.

Linting is another useful tool to educate new developers, and as a bonus, it helps maintain consistent and idiomatic code. JavaScript Linters prevent common pitfalls even without type systems, but are stronger at protecting code with a type system, which is why we use TSLint with all of our TypeScript projects. TSLint can do more than standard linters; for example, TSLint can catch interesting “gotchas!” like awaiting-non-promises, something not possible with vanilla JavaScript linters.

In the process of training engineers to use TypeScript and iteratively adding TypeScript to our codebase, we discovered that much of our JavaScript code did not easily lend itself to typing. While frustrating, discovering these scenarios is a good indicator that code is not well architected, which helped us prioritize refactoring.

Writing with TypeScript at Lyft

Since we adopted TypeScript, many new Lyft projects are TypeScript-only. These are just a few samples of projects where we’ve adopted TypeScript.

TypeScript React Convertor

React has a runtime-based type system for its components: PropTypes, a powerful tool for enforcing interface of shared components. We use PropTypes extensively at Lyft for our non-TypeScript repositories.

But in TypeScript, runtime type checking is unnecessary and all of type-checking happens via the TypeScript compiler. That’s why we invested time in developing a React JavaScript-to-TypeScript converter. It takes advantage of TypeScript compiler transformers and converts all PropTypes in a React component into TypeScript interfaces. This project rapidly accelerated our adoption of TypeScript, saving us hundreds of engineer-hours.

Universal Async Component

In this project we tried to solve the problem of rendering dynamically loaded components on the server-side. It’s written entirely in TypeScript, and represents a good example of how to write shared code with TypeScript by leveraging shared type definitions.

Our CSS framework TypeScript Integration

At Lyft, we use an internal atomic CSS library called Tetris. The methodology behind atomic CSS is couched in the idea that repeating class names is usually cheaper than repeating CSS code. Atomic CSS frameworks introduce many small CSS classes that can be combined to style an element. When using Tetris, developers must remember countless CSS class names (e.g., like p-a-m, which adds padding around a box).

To enhance our developers’ productivity — and reduce the need for perfect memory — we defined a single TypeScript file that exports every Tetris class name. Now Lyft developers can import this file and use their favorite editor’s autocomplete feature to get hints for class names:

Swagger to TypeScript Code Generation

One of the most intractable categories of bugs are those that frequently appear when working with backend APIs. A backend change can break frontend code — or sometimes frontend code changes break compatibility with backend API schemas.

Our backend APIs are described with OpenAPI (Swagger 2.0). Creating a formal specification in this manner gives us the perfect opportunity for strongly typing our API usage in frontend applications. We use Swagger JSON Schema models to automatically generate TypeScript interfaces for our API clients.

Want to work with TypeScript at Lyft?

We hope this gave you some perspective about why Lyft chose TypeScript over FlowType, and how we’ve begun our transition to strongly typed JavaScript. If you’re still intrigued, and want to work with TypeScript at Lyft, we’re hiring!



Source link