Navigation
Recherche
|
How to gracefully migrate your JavaScript programs to TypeScript
mercredi 7 mai 2025, 11:00 , par InfoWorld
TypeScript is a variant of JavaScript that provides strong type information, a powerful development tool that minimizes bugs and makes JavaScript programs easier to build in enterprise settings. TypeScript runs wherever JavaScript does and compiles to JavaScript itself. And, any existing JavaScript program is already valid TypeScript, just without the type information TypeScript provides.
All of this means you can take an existing JavaScript program and transform it into TypeScript, and you can do it incrementally. You don’t have to scrap all your JavaScript code and start from a blank page; you can work with your existing JavaScript codebase and migrate it a little at a time. Or, if you want, you can begin completely from scratch and write new code directly in TypeScript, with all its type-checking features dialed up. Setting up the TypeScript compiler TypeScript is a completely separate project from JavaScript, so you can’t work with TypeScript properly without including its compiler. To get set up with TypeScript, you’ll need Node.js and npm: npm install -g typescript You can also use other projects in the JavaScript ecosystem to work with TypeScript. Bun, for instance, bundles the TypeScript compiler automatically, so there’s nothing else to install. The Deno runtime also has TypeScript support built in. If you’ve been mulling making the jump to one of those projects anyway, why not do it with TypeScript? Compiling TypeScript to JavaScript The most basic way to compile existing JavaScript to TypeScript is to just run the TypeScript compiler on some JavaScript code: tsc myfile.ts TypeScript files use.ts as their file extension. When run through the compiler, they are transformed into.js files of the same name in the same place. This is fine for one-offs, but you most likely have a whole project directory of files that need compiling. To do that without excess hassle, you’ll need to write a simple configuration file for your project to work with TypeScript’s compiler. TypeScript’s config file is typically named tsconfig.json, and lives in the root directory for your project. A simple tsconfig.json might look like this: { 'compilerOptions': { 'outDir': './jssrc', 'allowJs': true, 'target': 'es6', 'sourceMap': true }, 'include': ['./src/**/*'] } The first section, compilerOptions, tells the compiler where to place a compiled file. 'outDir': './jssrc' means all the generated.js files will be placed in a directory named jssrc (for “JavaScript source,” but you can use any name that fits your project layout). It also specifies that it will accept regular JavaScript files as input ('allowJs': true), so that you can mingle JavaScript and TypeScript files freely in your src folder without issues. If you don’t specify outDir, the JavaScript files will be placed side-by-side with their corresponding TypeScript files in your source directory. You may not want to do this for a variety of reasons—for instance, you might want to keep the generated files in a separate directory to make them easier to clean up. Our config file also lets us define what ECMAScript standard to compile to. 'target': 'es6' means we use ECMAScript 6. Most JavaScript engines and browsers now support ES6, so it’s an acceptable default. Specifying 'sourceMap': true generates.js.map files along with all your generated JavaScript files, for debugging. Lastly, the include section provides a glob pattern for where to find source files to process. tsconfig.json has tons more options beyond these, but the few mentioned here should be plenty to get up and running. Once you set up tsconfig.json, you can just run tsc in the root of the project and generate files in the directory specified by outDir. Try setting this up now in a copy of a JavaScript-based project you currently have, with at least one.ts-extension file. Be sure to specify an outDir that doesn’t conflict with anything else. That way, the changed files built by the compiler will not touch your existing files, so you can experiment without destroying any of your existing work. Adding TypeScript type annotations to existing code Once you’ve set up the compiler, the next thing to do is start migrating your existing code to TypeScript. Since every existing JavaScript file is already valid TypeScript, you can work a file at a time, incrementally, by renaming existing.js files as.ts. If a JavaScript file has no type information or other TypeScript-specific syntax, the TypeScript compiler will not do anything with it. The compiler only starts scrutinizing the code when there’s something TypeScript-specific about it. One way to get started with TypeScript annotations is by adding them to function signatures and return types. Here’s a JavaScript function with no type annotations; it’s a way to generate a person’s name in a last-name-first format, based on some object with.firstName and.lastName properties. function lastNameFirst(person) { return `${person.lastName}, ${person.firstName}`; } TypeScript lets us be much more explicit about what’s accepted and returned. We just provide type annotations for the arguments and return value: function lastNameFirst(person: Person): string { return `${person.lastName}, ${person.firstName}`; } This code assumes we have an object type named Person defined earlier in the code. It also leverages string as a built-in type to both JavaScript and TypeScript. By adding these annotations, we now ensure any code that calls this function must supply an object of type Person. If we have code that calls this function and supplies DogBreed instead, the compiler will complain: error TS2345: Argument of type 'DogBreed' is not assignable to parameter of type 'Person'. Type 'DogBreed' is missing the following properties from type 'Person': firstName, lastName One nice thing about the error details is you get more than just a warning that it isn’t the correct type—you also get notes as to why that type doesn’t work for a particular instance. This doesn’t just hint at how to fix the immediate problem but also lets us think about how our types could be made broader or narrower to fit our use cases. Interface declarations Another way to describe what types can be used with something is via an interface declaration. An interface describes what can be expected from a given thing, without needing to define that thing in full. For instance: interface Name { firstName: string; lastName: string; } function lastNameFirst(person: Name): string { return `${person.lastName}, ${person.firstName}`; } In a case like this, we could supply any type we wanted to lastnameFirst(), as long as it had.firstName and.lastName as properties, and as long as they were string types. This lets you create types that are about the type shape of the thing you’re using, rather than whether it’s some specific type. Figuring out what TypeScript types to use When you annotate JavaScript code to create TypeScript, most of the type information you apply will be familiar, since they’ll come from JavaScript types. But how and where you apply those types will take some thought. Generally, you don’t need to provide annotations for literals since those can be inferred automatically. For instance, name: string = 'Davis'; is redundant, since it’s clear from the assignment to the literal that name will be a string. (Many anonymous functions can also have their types inferred in this way.) The primitive types—string, number, and boolean—can be applied to variables that use those types and where they can’t be inferred automatically. For arrays of types, you can use the type followed by []—e.g., number[] for an array of numbers—or you can use the syntax Array (in this case, Array). When you want to define your own type, you can do so with the type keyword: type FullName = { firstName: string; lastName: string; }; This could in turn be used to create an object that matches its type shape: var myname:FullName = {firstName:'Brad', lastName:'Davis'}; However, this would generate an error: var myname:FullName = {firstName:'Brad', lastName:'Davis', middleName:'S.'}; The reason for the error is that the middleName isn’t defined in our type. You can use the | operator to indicate that something can be one of several possible types: type userName = Fullname | string; // or we can use it in a function signature... function doSomethingWithName(name: Fullname|string) {...} If you want to create a new type that’s a composite of existing types (an “intersection” type), use the & operator: type Person = { firstName: string; lastName: string; }; type Bibliography = { books: Array; }; type Author = Person & Bibliography; // we can then create an object that uses fields from both types: var a: Author = { firstName: 'Serdar', lastName: 'Yegulalp', books: ['Python Made Easy', 'Python Made Complicated'] }; Note that while you can do this with types, if you want to do something similar with interfaces you need to use a different approach; namely, using the extends keyword: interface Person { firstName: string; lastName: string; } interface Author extends Person { penName: string; } JavaScript classes are also respected as types. TypeScript lets you use them as-is with type annotations: class Person { name: string; constructor( public firstName: string, public lastName: string ) { this.firstName = firstName; this.lastName = lastName; this.name = `${firstName} ${lastName}`; } } TypeScript also has a few special types for dealing with other cases. any means just that: any type is accepted. null and undefined mean the same things as in regular JavaScript. For instance, you’d use string|null to indicate a type that would either be a string or a null value. TypeScript also natively supports the! postfix operator. For instance, x!.action() will assert in TypeScript that.action() will be called on x as long as x isn’t null or undefined. If you want to refer to a function that has a certain signature way of a type expression, you can use what’s called a “call signature”: function runFn(fn: (arg: number) => any, value: number): any { return fn(value); } runFn would accept a function that takes a single number as an argument and returns any value. runFn would take in such a function, plus a number value, and then execute that function with the value. Note that here we use the arrow notation to indicate what the passed function returns, not a colon as we do the main function signature. Building a TypeScript project Many build tools in the JavaScript ecosystem are now TypeScript-aware. For instance, the frameworks tsdx, Angular, and Nest all know how to automatically turn a TypeScript codebase into its matching JavaScript code with little intervention on your part. If you’re working with a build tool like Babel or webpack (among others), those tools can also handle TypeScript projects, as long as you install TypeScript handling as an extension or enable it manually. For instance, with webpack, you’d install the ts-loader package through npm, and then set up a webpack.config.js file to include your.ts files. The key to moving an existing JavaScript project to TypeScript is to approach it a step at a time—migrate one module at a time, then one function at a time. Because TypeScript can coexist with regular JavaScript, you are not obliged to migrate everything at once, and you can take the time to experiment with figuring out the best types to use across your project’s codebase.
https://www.infoworld.com/article/3973790/how-to-sensibly-migrate-your-javascript-programs-to-typesc...
Voir aussi |
56 sources (32 en français)
Date Actuelle
jeu. 8 mai - 19:53 CEST
|