Migrating from JavaScript

TypeScript doesn’t exist in a vacuum. It was built with the JavaScript ecosystem in mind, and a lot of JavaScript exists today. Converting a JavaScript codebase over to TypeScript is, while somewhat tedious, usually not challenging. In this tutorial, we’re going to look at how you might start out. We assume you’ve read enough of the handbook to write new TypeScript code.

设置您的目录

If you’re writing in plain JavaScript, it’s likely that you’re running your JavaScript directly, where your.jsfiles in asrc,lib, ordistdirectory, and then ran as desired.

If that’s the case, the files that you’ve written are going to be used as inputs to TypeScript, and you’ll run the outputs it produces. During our JS to TS migration, we’ll need to separate our input files to prevent TypeScript from overwriting them. If your output files need to reside in a specific directory, then that will be your output directory.

You might also be running some intermediate steps on your JavaScript, such as bundling or using another transpiler like Babel. In this case, you might already have a folder structure like this set up.

From this point on, we’re going to assume that your directory is set up something like this:

projectRoot
├── src
│   ├── file1.js
│   └── file2.js
├── built
└── tsconfig.json

If you have atestsfolder outside of yoursrcdirectory, you might have onetsconfig.jsoninsrc, and one intestsas well.

写入配置文件

TypeScript uses a file calledtsconfig.jsonfor managing your project’s options, such as which files you want to include, and what sorts of checking you want to perform. Let’s create a bare-bones one for our project:

{
    "compilerOptions": {
        "outDir": "./built",
        "allowJs": true,
        "target": "es5"
    },
    "include": [
        "./src/**/*"
    ]
}

Here we’re specifying a few things to TypeScript:

  1. Read in any files it understands in thesrcdirectory (withinclude).
  2. Accept JavaScript files as inputs (withallowJs).
  3. Emit all of the output files inbuilt(withoutDir).
  4. Translate newer JavaScript constructs down to an older version like ECMAScript 5 (usingtarget).

At this point, if you try runningtscat the root of your project, you should see output files in thebuiltdirectory. The layout of files inbuiltshould look identical to the layout ofsrc. You should now have TypeScript working with your project.

早期福利

Even at this point you can get some great benefits from TypeScript understanding your project. If you open up an editor likeVS CodeorVisual Studio, you’ll see that you can often get some tooling support like completion. You can also catch certain bugs with options like:

  • noImplicitReturnswhich prevents you from forgetting to return at the end of a function.
  • noFallthroughCasesInSwitchwhich is helpful if you never want to forget abreakstatement betweencases in aswitchblock.

TypeScript will also warn about unreachable code and labels, which you can disable withallowUnreachableCodeandallowUnusedLabelsrespectively.

与构建工具集成

You might have some more build steps in your pipeline. Perhaps you concatenate something to each of your files. Each build tool is different, but we’ll do our best to cover the gist of things.

Gulp

If you’re using Gulp in some fashion, we have a tutorial onusing Gulpwith TypeScript, and integrating with common build tools like Browserify, Babelify, and Uglify. You can read more there.

Webpack

Webpack integration is pretty simple. You can useawesome-typescript-loader, a TypeScript loader, combined withsource-map-loaderfor easier debugging. Simply run

npm install awesome-typescript-loader source-map-loader

and merge in options from the following into yourwebpack.config.jsfile:

module.exports = {
    entry: "./src/index.ts",
    output: {
        filename: "./dist/bundle.js",
    },

    // Enable sourcemaps for debugging webpack's output.
    devtool: "source-map",

    resolve: {
        // Add '.ts' and '.tsx' as resolvable extensions.
        extensions: ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js"]
    },

    module: {
        loaders: [
            // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
            { test: /\.tsx?$/, loader: "awesome-typescript-loader" }
        ],

        preLoaders: [
            // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
            { test: /\.js$/, loader: "source-map-loader" }
        ]
    },

    // Other options...
};

It’s important to note that awesome-typescript-loader will need to run before any other loader that deals with.jsfiles.

The same goes forts-loader, another TypeScript loader for Webpack. You can read more about the differences between the twohere.

You can see an example of using Webpack in ourtutorial on React and Webpack.

移动到TypeScript文件

At this point, you’re probably ready to start using TypeScript files. The first step is to rename one of your.jsfiles to.ts. If your file uses JSX, you’ll need to rename it to.tsx.

Finished with that step? Great! You’ve successfully migrated a file from JavaScript to TypeScript!

Of course, that might not feel right. If you open that file in an editor with TypeScript support (or if you runtsc --pretty), you might see red squiggles on certain lines. You should think of these the same way you’d think of red squiggles in an editor like Microsoft Word. TypeScript will still translate your code, just like Word will still let you print your documents.

If that sounds too lax for you, you can tighten that behavior up. If, for instance, you_don’t_want TypeScript to compile to JavaScript in the face of errors, you can use thenoEmitOnErroroption. In that sense, TypeScript has a dial on its strictness, and you can turn that knob up as high as you want.

If you plan on using the stricter settings that are available, it’s best to turn them on now (seeGetting Stricter Checksbelow). For instance, if you never want TypeScript to silently inferanyfor a type without you explicitly saying so, you can usenoImplicitAnybefore you start modifying your files. While it might feel somewhat overwhelming, the long-term gains become apparent much more quickly.

消除错误

Like we mentioned, it’s not unexpected to get error messages after conversion. The important thing is to actually go one by one through these and decide how to deal with the errors. Often these will be legitimate bugs, but sometimes you’ll have to explain what you’re trying to do a little better to TypeScript.

从模块导入

You might start out getting a bunch of errors likeCannot find name 'require'., andCannot find name 'define'.. In these cases, it’s likely that you’re using modules. While you can just convince TypeScript that these exist by writing out

// For Node/CommonJS
declare function require(path: string): any;

or

// For RequireJS/AMD
declare function define(...args: any[]): any;

it’s better to get rid of those calls and use TypeScript syntax for imports.

First, you’ll need to enable some module system by setting TypeScript’smoduleflag. Valid options arecommonjs,amd,system, andumd.

If you had the following Node/CommonJS code:

var foo = require("foo");

foo.doStuff();

or the following RequireJS/AMD code:

define(["foo"], function(foo) {
    foo.doStuff();
})

then you would write the following TypeScript code:

import foo = require("foo");

foo.doStuff();

获取声明文件

If you started converting over to TypeScript imports, you’ll probably run into errors likeCannot find module 'foo'.. The issue here is that you likely don’t have_declaration files_to describe your library. Luckily this is pretty easy. If TypeScript complains about a package likelodash, you can just write

npm install -s @types/lodash

If you’re using a module option other thancommonjs, you’ll need to set yourmoduleResolutionoption tonode.

After that, you’ll be able to import lodash with no issues, and get accurate completions.

从模块导出

Typically, exporting from a module involves adding properties to a value likeexportsormodule.exports. TypeScript allows you to use top-level export statements. For instance, if you exported a function like so:

module.exports.feedPets = function(pets) {
    // ...
}

you could write that out as the following:

export function feedPets(pets) {
    // ...
}

Sometimes you’ll entirely overwrite the exports object. This is a common pattern people use to make their modules immediately callable like in this snippet:

var express = require("express");
var app = express();

You might have previously written that like so:

function foo() {
    // ...
}
module.exports = foo;

In TypeScript, you can model this with theexport =construct.

function foo() {
    // ...
}
export = foo;

参数太多/太少

You’ll sometimes find yourself calling a function with too many/few arguments. Typically, this is a bug, but in some cases, you might have declared a function that uses theargumentsobject instead of writing out any parameters:

function myCoolFunction() {
    if (arguments.length == 2 && !Array.isArray(arguments[1])) {
        var f = arguments[0];
        var arr = arguments[1];
        // ...
    }
    // ...
}

myCoolFunction(function(x) { console.log(x) }, [1, 2, 3, 4]);
myCoolFunction(function(x) { console.log(x) }, 1, 2, 3, 4);

In this case, we need to use TypeScript to tell any of our callers about the waysmyCoolFunctioncan be called using function overloads.

function myCoolFunction(f: (x: number) => void, nums: number[]): void;
function myCoolFunction(f: (x: number) => void, ...nums: number[]): void;
function myCoolFunction() {
    if (arguments.length == 2 && !Array.isArray(arguments[1])) {
        var f = arguments[0];
        var arr = arguments[1];
        // ...
    }
    // ...
}

We added two overload signatures tomyCoolFunction. The first checks states thatmyCoolFunctiontakes a function (which takes anumber), and then a list ofnumbers. The second one says that it will take a function as well, and then uses a rest parameter (...nums) to state that any number of arguments after that need to benumbers.

顺序添加的属性

Some people find it more aesthetically pleasing to create an object and add properties immediately after like so:

var options = {};
options.color = "red";
options.volume = 11;

TypeScript will say that you can’t assign tocolorandvolumebecause it first figured out the type ofoptionsas{}which doesn’t have any properties. If you instead moved the declarations into the object literal themselves, you’d get no errors:

let options = {
    color: "red",
    volume: 11
};

You could also define the type ofoptionsand add a type assertion on the object literal.

interface Options { color: string; volume: number }

let options = {} as Options;
options.color = "red";
options.volume = 11;

Alternatively, you can just sayoptionshas the typeanywhich is the easiest thing to do, but which will benefit you the least.

any,Object, and{}

You might be tempted to useObjector{}to say that a value can have any property on it becauseObjectis, for most purposes, the most general type. Howeveranyis actually the type you want to usein those situations, since it’s the most_flexible_type.

For instance, if you have something that’s typed asObjectyou won’t be able to call methods liketoLowerCase()on it. Being more general usually means you can do less with a type, butanyis special in that it is the most general type while still allowing you to do anything with it. That means you can call it, construct it, access properties on it, etc. Keep in mind though, whenever you useany, you lose out on most of the error checking and editor support that TypeScript gives you.

If a decision ever comes down toObjectand{}, you should prefer{}. While they are mostly the same, technically{}is a more general type thanObjectin certain esoteric cases.

获得更严格的检查

TypeScript comes with certain checks to give you more safety and analysis of your program. Once you’ve converted your codebase to TypeScript, you can start enabling these checks for greater safety.

No Implicitany

There are certain cases where TypeScript can’t figure out what certain types should be. To be as lenient as possible, it will decide to use the typeanyin its place. While this is great for migration, usinganymeans that you’re not getting any type safety, and you won’t get the same tooling support you’d get elsewhere. You can tell TypeScript to flag these locations down and give an error with thenoImplicitAnyoption.

Strictnull&undefinedChecks

By default, TypeScript assumes thatnullandundefinedare in the domain of every type. That means anything declared with the typenumbercould benullorundefined. Sincenullandundefinedare such a frequent source of bugs in JavaScript and TypeScript, TypeScript has thestrictNullChecksoption to spare you the stress of worrying about these issues.

WhenstrictNullChecksis enabled,nullandundefinedget their own types callednullandundefinedrespectively. Whenever anything ispossiblynull, you can use a union type with the original type. So for instance, if something could be anumberornull, you’d write the type out asnumber | null.

If you ever have a value that TypeScript thinks is possiblynull/undefined, but you know better, you can use the postfix!operator to tell it otherwise.

declare var foo: string[] | null;

foo.length;  // error - 'foo' is possibly 'null'

foo!.length; // okay - 'foo!' just has type 'string[]'

As a heads up, when usingstrictNullChecks, your dependencies may need to be updated to usestrictNullChecksas well.

No Implicitanyforthis

When you use thethiskeyword outside of classes, it has the typeanyby default. For instance, imagine aPointclass, and imagine a function that we wish to add as a method:

class Point {
    constuctor(public x, public y) {}
    getDistance(p: Point) {
        let dx = p.x - this.x;
        let dy = p.y - this.y;
        return Math.sqrt(dx ** 2 + dy ** 2);
    }
}
// ...

// Reopen the interface.
interface Point {
    distanceFromOrigin(point: Point): number;
}
Point.prototype.distanceFromOrigin = function(point: Point) {
    return this.getDistance({ x: 0, y: 0});
}

This has the same problems we mentioned above - we could easily have misspelledgetDistanceand not gotten an error. For this reason, TypeScript has thenoImplicitThisoption. When that option is set, TypeScript will issue an error whenthisis used without an explicit (or inferred) type. The fix is to use athis-parameter to give an explicit type in the interface or in the function itself:

Point.prototype.distanceFromOrigin = function(this: Point, point: Point) {
    return this.getDistance({ x: 0, y: 0});
}

results matching ""

    No results matching ""