目录
5、将 JavaScript 代码迁移至 TypeScript
1、编译上下文(tsconfig.json)
基础使用编译选项
{}
通过compileroptions来定制你的编译选项
{
"compilerOptions": {
/* 基本选项 */
"target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
"module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
"lib": [], // 指定要包含在编译中的库文件
"allowJs": true, // 允许编译 javascript 文件
"checkJs": true, // 报告 javascript 文件中的错误
"jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
"declaration": true, // 生成相应的 '.d.ts' 文件
"sourceMap": true, // 生成相应的 '.map' 文件
"outFile": "./", // 将输出文件合并为一个文件
"outDir": "./", // 指定输出目录
"rootDir": "./", // 用来控制输出目录结构 --outDir.
"removeComments": true, // 删除编译后的所有的注释
"noEmit": true, // 不生成输出文件
"importHelpers": true, // 从 tslib 导入辅助工具函数
"isolatedModules": true, // 将每个文件作为单独的模块 (与 'ts.transpileModule' 类似).
/* 严格的类型检查选项 */
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
"strictNullChecks": true, // 启用严格的 null 检查
"noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
"alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'
/* 额外的检查 */
"noUnusedLocals": true, // 有未使用的变量时,抛出错误
"noUnusedParameters": true, // 有未使用的参数时,抛出错误
"noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
"noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)
/* 模块解析选项 */
"moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
"baseUrl": "./", // 用于解析非相对模块名称的基目录
"paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
"rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
"typeRoots": [], // 包含类型声明的文件列表
"types": [], // 需要包含的类型声明文件名列表
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。
/* Source Map Options */
"sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
"mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
"inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
"inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性
/* 其他选项 */
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true // 为装饰器提供元数据的支持
}
}
命令行手动运行ts编译器
- 运行 tsc,它会在当前目录或者是父级目录寻找
tsconfig.json
文件。 - 运行
tsc -p ./path-to-project-directory
。当然,这个路径可以是绝对路径,也可以是相对于当前目录的相对路径。
你甚至可以使用 tsc -w
来启用 TypeScript 编译器的观测模式,在检测到文件改动之后,它将重新编译。
指定文件
{
"files": [
"./some/file.ts"
], // 显示指定需要编译的文件
"include": [
"./folder"
], // 需要包含的文件
"exclude": [
"./folder/**/*.spec.ts",
"./folder/someSubFolder"
] // 需要删除的文件
}
2、声明空间
类型声明空间
// 类型声明
class Foo {}
interface Bar {}
type Bas = {};
// 可以将 Foo, Bar, Bas 作为类型注解使用
let foo: Foo;
let bar: Bar;
let bas: Bas;
// 注意,尽管你定义了interface Bar,却并不能够把它作为一个变量来使用,因为它没有定义在变量声明空间中
// 我们并不能把一些如 interface 定义的内容当作变量使用
interface Bar {}
const bar = Bar; // Error: "cannot find name 'Bar'"
// 与此相似,一些用 var 声明的变量,也只能在变量声明空间使用,不能用作类型注解
const foo = 123;
let bar: foo; // ERROR: "cannot find name 'foo'"
// 提示 ERROR: "cannot find name 'foo'" 原因是,名称 foo 没有定义在类型声明空间里
变量声明空间
// 变量声明空间包含可用作变量的内容,在上文中 Class Foo 提供了一个类型 Foo 到类型声明空间,此外它同样提供了一个变量 Foo 到变量声明空间
class Foo {}
const someVar = Foo;
const someOtherVar = 123;
3、模块
全局模块
// 在一个新的ts文件中直接声明变量,它处于全局命名空间中,那么其他同项目的ts文件也可以直接使用
const foo = 123;
// 另一个ts文件调用
const bar = foo; // allowed
文件模块
// 在你的 TypeScript 文件的根级别位置含有 import 或者 export,那么它会在这个文件中创建一个本地的作用域,此时全局命名空间里就不再有foo
export const foo = 123;
// 另一个ts文件调用
const bar = foo; // ERROR: "cannot find name 'foo'"
// 如果在其他文件想要对foo进行调用,则必须显示地将其导入
import { foo } from './foo';
const bar = foo; // allow
// 在新的ts文件里使用import时,它不仅允许你使用从其他文件导入的内容,还会将此文件标记为一个模块,文件内定义的声明也不会“污染”全局命名空间
文件模块详情
使用 module: commonjs
选项以及使用 ES 模块语法导入、导出、编写模块
ES模块语法
// 使用 export 关键字导出一个变量或类型
// foo.ts
export const someVar = 123;
export type someType = {
foo: string;
};
// foo.ts
const someVar = 123;
type someType = {
type: string;
};
export { someVar, someType };
// 重命名变量的方式
// foo.ts
const someVar = 123;
export { someVar as aDifferentName };
// 使用import导入一个变量或者类型
// bar.ts
import { someVar, someType } from './foo';
// 通过重命名的方式导入变量或者类型
// bar.ts
import { someVar as aDifferentName } from './foo';
// 整体加载
// bar.ts
import * as foo from './foo';
// 你可以使用 `foo.someVar` 和 `foo.someType` 以及其他任何从 `foo` 导出的变量或者类型
// 只导入模块
import 'core-js'; // 一个普通的 polyfill 库
// 从其他模块导入后整体导出
export * from './foo';
// 从其他模块导入后部分导出
export { someVar } from './foo';
// 通过重命名,部分导出从另一个模块导入的项目
export { someVar as aDifferentName } from './foo';
默认导入/导出
// 使用 export default
// 在一个变量之前(不需要使用 let/const/var)
// 在一个函数之前
// 在一个类之前
// some var
export default (someVar = 123);
// some function
export default function someFunction() {}
// some class
export default class someClass {}
// 导入使用 import someName from 'someModule' 语法(你可以根据需要为导入命名)
import someLocalNameForThisFile from './foo';
4、TypeScript类型系统
基本类型注解
let num: number;
let str: string;
let bool: boolean;
num = 123;
num = 123.456;
num = '123'; // Error
str = '123';
str = 123; // Error
bool = true;
bool = false;
bool = 'false'; // Error
数组类型注解
// 可以根据需要补充任何有效的类型注解(如::boolean[])。它能让你安全的使用任何有关数组的操作,而且它也能防止一些类似于赋值错误类型给成员的行为
let boolArray: boolean[];
boolArray = [true, false];
console.log(boolArray[0]); // true
console.log(boolArray.length); // 2
boolArray[1] = true;
boolArray = [false, false];
boolArray[0] = 'false'; // Error
boolArray = 'false'; // Error
boolArray = [true, 'false']; // Error
接口类型注解
// 接口是 TypeScript 的一个核心知识,它能合并众多类型声明至一个类型声明
interface Name {
first: string;
second: number;
third: boolean;
}
let name: Name;
name = {
first: 'John',
second: 2,
third: true
};
name = {
// Error: 'Second is missing'
first: 'John',
third: true
};
name = {
// Error: 'Second is the wrong type'
first: 'John',
second: false,
third: false
};
内联类型注解
// 内联类型能为你快速的提供一个类型注解。它可以帮助你省去为类型起名的麻烦(你可能会使用一个很糟糕的名称)。然而,如果你发现需要多次使用相同的内联注解时,那么考虑把它重构为一个接口(或者是 type alias,它会在接下来的部分提到)是一个不错的主意
let name: {
first: string;
second: string;
};
name = {
first: 'John',
second: 'Doe'
};
name = {
// Error: 'Second is missing'
first: 'John'
};
name = {
// Error: 'Second is the wrong type'
first: 'John',
second: 1337
};
特殊类型注解
// any (能不用就不用,尽量少用)
// any 类型在 TypeScript 类型系统中占有特殊的地位。它提供给你一个类型系统的「后门」,TypeScript 将会把类型检查关闭。在类型系统里 any 能够兼容所有的类型(包括它自己)。因此,所有类型都能被赋值给它,它也能被赋值给其他任何类型
let power: any;
// 赋值任意类型
power = '123';
power = 123;
// 它也兼容任何类型
let num: number;
power = num;
num = power;
// null和undefined
// 在类型系统中,JavaScript 中的 null 和 undefined 字面量和其他被标注了 any 类型的变量一样,都能被赋值给任意类型的变量
let num: number;
let str: string;
// 这些类型能被赋予
num = null;
str = undefined;
// void 使用 :void 来表示一个函数没有一个返回值
function log(message: string): void {
console.log(message);
}
泛型
// 函数reverse接受一个类型为T(注意在 reverse<T> 中的类型参数)的数组(items: T[]),返回值为类型T的一个数组(注意:T[]),函数reverse的返回值类型与它接受的参数的类型一样。当你传入const sample = [1, 2, 3]时,TypeScript能推断出reverse为number[]类型,从而能给你类型安全。与此相似,当你传入一个类型为string[]类型的数组时,TypeScript能推断reverse为string[]类型
function reverse<T>(items: T[]): T[] {
const toreturn = [];
for (let i = items.length - 1; i >= 0; i--) {
toreturn.push(items[i]);
}
return toreturn;
}
const sample = [1, 2, 3];
let reversed = reverse(sample); // 给予类型安全
console.log(reversed); // 3, 2, 1
// Safety
reversed[0] = '1'; // Error
reversed = ['1', '2']; // Error
reversed[0] = 1; // ok
reversed = [1, 2]; // ok
const strArr = ['1', '2'];
let reversedStrs = reverse(strArr); // 推断reverse为string[]类型
reversedStrs = [1, 2]; // Error
// reverse方法结构
interface Array<T> {
reverse(): T[];
}
// 这就意味着当你在数组上调用.reverse方法时,将会获得类型安全
let numArr = [1, 2];
let reversedNums = numArr.reverse();
reversedNums = ['1', '2']; // Error
联合类型
// 属性为多种类型之一,如字符串或者数组。这正是 TypeScript 中联合类型能派上用场的地方(它使用 | 作为标记,如 string | number)
function formatCommandline(command: string[] | string) {
let line = '';
if (typeof command === 'string') {
line = command.trim();
} else {
line = command.join(' ').trim();
}
// Do stuff with line: string
}
交叉类型
// 可以从两个对象中创建一个新对象,新对象拥有着两个对象所有的功能
function extend<T extends object, U extends object>(first: T, second: U): T & U {
const result = <T & U>{};
for (let id in first) {
(<T>result)[id] = first[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<U>result)[id] = second[id];
}
}
return result;
}
const x = extend({ a: 'hello' }, { b: 42 });
// 现在 x 拥有了 a 属性与 b 属性
const a = x.a;
const b = x.b;
元祖类型
// JavaScript 并不支持元组,开发者们通常只能使用数组来表示元组。而 TypeScript 支持它,开发者可以使用 :[typeofmember1, typeofmember2] 的形式,为元组添加类型注解,元组可以包含任意数量的成员
let nameNumber: [string, number];
// Ok
nameNumber = ['Jenny', 221345];
// Error
nameNumber = ['Jenny', '221345'];
// 将其与ts中的解构一起使用
let nameNumber: [string, number];
nameNumber = ['Jenny', 322134];
const [name, num] = nameNumber;
类型别名
// 类型多样的时候可以自定义别名进行代码优化
type StrOrNum = string | number;
// 使用
let sample: StrOrNum;
sample = 123;
sample = '123';
// 会检查类型
sample = true; // Error
// 与接口不同,你可以为任意的类型注解提供类型别名(在联合类型和交叉类型中比较实用)
type Text = string | { text: string };
type Coordinates = [number, number];
type Callback = (data: string) => void;
5、将 JavaScript 代码迁移至 TypeScript
- 添加一个
tsconfig.json
文件; - 把文件扩展名从
.js
改成.ts
,开始使用any
来减少错误; - 开始在 TypeScript 中写代码,尽可能的减少
any
的使用; - 回到旧代码,开始添加类型注解,并修复已识别的错误;
- 为第三方 JavaScript 代码定义环境声明。
6、枚举
// 简单定义一个枚举
// 数字型枚举
// 默认情况下,第一个枚举值是 0,然后每个后续值依次递增 1
enum CardSuit {
Clubs,
Diamonds,
Hearts,
Spades
}
// 简单的使用枚举类型
let Card = CardSuit.Clubs;
// 类型安全
Card = 'not a member of card suit'; // Error: string 不能赋值给 `CardSuit` 类型
card = 90; // success
// 定义一个字符串枚举
export enum EvidenceTypeEnum {
UNKNOWN = '',
PASSPORT_VISA = 'passport_visa',
PASSPORT = 'passport',
SIGHTED_STUDENT_CARD = 'sighted_tertiary_edu_id',
SIGHTED_KEYPASS_CARD = 'sighted_keypass_card',
SIGHTED_PROOF_OF_AGE_CARD = 'sighted_proof_of_age_card'
}
7、类型断言
// 这里的代码发出了错误警告,因为 foo 的类型推断为 {},即没有属性的对象。因此,你不能在它的属性上添加 bar 或 bas
const foo = {};
foo.bar = 123; // Error: 'bar' 属性不存在于 ‘{}’
foo.bas = 'hello'; // Error: 'bas' 属性不存在于 '{}'
// 类型断言
interface Foo {
bar: number;
bas: string;
}
const foo = {} as Foo;
foo.bar = 123;
foo.bas = 'hello';
// 上述类型断言存在一个弊端,就是忘记定义foo的属性时,编译器不会报错
// 可以改成以下方式
interface Foo {
bar: number;
bas: string;
}
const foo: Foo = {
// 编译器将会提供 Foo 属性的代码提示
};
// 类型断言不安全的原因,当当使用者了解传入参数更具体的类型时,类型断言能按预期工作,反之则不能,会报错,解决方法:双重断言
function handler(event: Event) {
const element = event as HTMLElement; // Error: 'Event' 和 'HTMLElement' 中的任何一个都不能赋值给另外一个
}
function handler(event: Event) {
const element = (event as any) as HTMLElement; // ok
}
8、Freshness
function logName(something: { name: string }) {
console.log(something.name);
}
const person = { name: 'matt', job: 'being awesome' };
const animal = { name: 'cow', diet: 'vegan, but has milk of own specie' };
const randow = { note: `I don't have a name property` };
logName(person); // ok
logName(animal); // ok
logName(randow); // Error: 没有 `name` 属性
logName({ name: 'matt' }); // ok
logName({ name: 'matt', job: 'being awesome' }); // Error: 对象字面量只能指定已知属性,`job` 属性在这里并不存在。
// 这种错误提示只会发生在对象字面量上
// 用例:React State
// 假设
interface State {
foo: string;
bar: string;
}
// 你可能想做:
this.setState({ foo: 'Hello' }); // Error: 没有属性 'bar'
// 因为 state 包含 'foo' 与 'bar',TypeScript 会强制你这么做:
this.setState({ foo: 'Hello', bar: this.state.bar });
// 如果想要避免上述的报错,只需要添加可选链符号?
interface State {
foo?: string;
bar?: string;
}
9、类型保护
typeof、instanceof、in、字面量类型保护、自定义类型保护
10、Readonly
标记属性为只读属性