最近在用 typescript + react 开发项目,学习到了蛮多。其实 typescript 是 js 的超集,最终还是要编译为 js 代码。typescript 提供的静态类型系统,大大增强了代码的可读性以及可维护性,可以更好地支持大规模 JavaScript 应用的开发和维护。
写在前面
JavaScript 是一门弱类型语言,变量的数据类型具有动态性,只有执行时才能确定变量的类型。以往的前端开发过程中,很容易出现以下的报错:
// 读取对象上不存在的属性
Uncaught TypeError: Cannot read property ... of undefined
// 类型错误
TypeError: 'undefined' is not a ...以对象 obj 为例,在 typescript 中可以用接口来定义一个对象类型
interface IObj {
  name: string
}
const obj: IObj;通过这种方式限制 obj 的属性类型,在赋值时只能是添加 name 属性,并且不能给 name 赋值 string 以外的类型;每当你获取 obj 属性的值时,会在编译阶段检查该属性是否存在,不存在会提前抛出类型错误。
在变量声明之前做好类型定义,并通过提前暴露和修复这些可能出现的类型错误,可以大大降低应用在运行时出现错误的概率,使得 javascript 应用更加健壮,这一切得益于类型思维。
作用总结为以下几点:
- 类型检查
- 提供更丰富的语法(es6)
- 编译器提示友好
思维方式决定了编程习惯,编程习惯奠定了工程质量,工程质量划定了能力边界
数据类型
number
数字类型,但是new Number(0)这种是对象,并不是数字类型
let num: number = 0;
let num = 0; // 这样也可以,ts 会进行隐性的类型推论,不过还是建议明确定义类型string
let str: string = "str";boolean
let bool: boolean = true;数组
主要有两种方式定义数组类型,一般只能定义数组元素都为某一种类型的数组
let arr: number[] = [1, 2, 3];
let arr: Array<number> = [1, 2, 3];
// 定义一个不确定元素类型的数组
let arr: any[] = [];
// 定义一个只读的数组
let arr: ReadonlyArray<number> = [1, 2];元组
元组可以用于定义一个具有多种类型元素的数组,元素数量已知,并且他们的顺序是确定的
const arr: [string, number] = ["cat", 1];
const arr: [string, number?] = ["cat", 1];
const arr: [string, number?] = ["cat"];
function fn(...args: [number, string]): void;TypeScript 4.0 支持为元组类型设置标签
function fn(...args: [age: number, id: string]): void;枚举
列出所有可用值,默认初始值是 0。常用于状态值管理等
enum MyPets {
  cat = "lazy cat",
  dog,
  mouse,
}
let cat: MyPets = MyPets.cat;枚举的原理其实也很简单,相当于定义对象时多做了 value—>key 的映射,以上面的代码为例
var MyPets;
(function (MyPets) {
    MyPets["cat"] = "lazy cat";
    MyPets[MyPets["dog"] = void 0] = "dog";
    MyPets[MyPets["mouse"] = void 0] = "mouse";
})(MyPets || (MyPets = {}));null
类似于 js 的 null,是所有类型的子类型
undefined
类似于 js 的 undefined,是所有类型的子类型
void
当函数没有返回值时,可以直接用 void 定义返回值类型。
function func(): void {
  ...
}声明一个 void 类型的变量没有什么用,因为你只能将它赋值为 undefined 和 null
unknown
所有类型都可以分配给 unknown,但只能将 unknown 类型的变量赋值给 any 和 unknown
any
声明一个 any 类型的变量,它可以赋给任何类型值,也可以接受任何类型值,也就意味着放弃对这个变量的类型检查,不建议使用。
let demo: any = 2; // 接受任何类型值
let demo_2: string;
demo_2 = demo; // 赋给任何类型值any 和 unknown 的最大区别是, unknown 是 top type (任何类型都是它的 subtype) , 而 any 即是 top type, 又是 bottom type (它是任何类型的 subtype ) ,这导致 any 基本上就是放弃了任何类型检查.
never
当一个函数永远不会返回时,我们可以声明返回值类型为 never
function func(): never {
  throw new Error("never reach");
}可以利用 never 在类型收窄时做全面性的检测,防止后面对 TPayload 的意外修改,确保类型安全
type TPayload = number | string;
function func(payload: TPayload) {
  if (typeof payload === "number") {
    return 0;
  } else if (typeof payload === "string") {
    return "";
  } else {
    const res: never = payload;
    return res;
  }
}接口
接口(interface)常用于定义对象类型
interface IObj {
  name: string;
  age?: number; // 可选
  readonly type: string; // 只读属性,只能在创建的时候赋值
  [key: string]: string; // 索引类型,允许定义多个key为string的string类型值
}同名接口会合并成员,如果有相同的成员,必须为同类型,否则编译器会报错。
interface IObj {
  name: string;
  func(id: string): string;
}
interface IObj {
  age: number;
  func(id: number): number;
}
// 相当于
interface IObj {
  name: string;
  age: number;
  func(id: number): number;
  func(id: string): string;
}更详细的合并规则可以参考 typescript 的声明合并
函数
定义入参和返回值类型,必传参数可具有默认值
// age是可选参数,需要放在必传参数name后面
function func (id: string = '007', num?: number): string {
  ...
}
let func: (a: string) => string = function(a: string): string{
    return 'JacksonZhou'
}也可以通过接口定义函数,分为 callable 和 newable 两种形式
// callable
interface Func {
  (id: number, ...others: any[]): string;
}
const fn: Func;
// newable
interface NewFunc {
  new (): string;
}
const fn: NewFunc;
new fn();函数重载
function func(a: number, b: number);
function func(a: string, b: string);
function func(a: number, b: number, c: number) {
  //...
}函数重载的原理是重新定义所有重名函数,并放到一个列表里,匹配函数时会从上到下,通过匹配参数类型来选择指定函数
交叉类型
let id: string & number;联合类型
联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型;
let id: string | number;
id = "2";
console.log(id.length);
id = 2;
console.log(id.length); // 报错,数字类型没有length属性类型断言
对类型有把握但是又不能准确定义的时候,可以使用类型断言
常规断言(<> or as):
const id: any = "hello";
const length = (id as string).length;
// const length = (<string>id).length;实际开发中使用的几率不大,比如下面这种情况就可以使用断言来规避编译报错
interface Cat {
  name: string;
  run(): void;
}
interface Fish {
  name: string;
  swim(): void;
}
function isFish(animal: Cat | Fish) {
  if (typeof (animal as Cat).run === "function") {
    return true;
  }
  return false;
}非空断言(!):
function fn(maybeString: string | undefined | null) {
  const onlyString1: string = maybeString; // Error
  const onlyString2: string = maybeString!; // Ok
}
function fn(getNum: () => number | undefined) {
  const num1 = getNum(); // Error
  const num2 = getNum!(); // OK
}类型守卫
类型守卫指的就是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内,主要有以下几种方法检测类型:
- in
- typeof
- instanceof
- is
function isNumber(x: any): x is number {
  return typeof x === "number";
}泛型
定义未来数据类型
function func<T>(arg: T): T {
  return arg;
}
let func_1 = func<string>("cat");
let func_2 = func("cat");类
typescript 对类属性的访问控制
| 可访问性 | public | protected | private | 
|---|---|---|---|
| 类 | yes | yes | yes | 
| 子类 | yes | yes | no | 
| 类实例 | yes | no | no | 
class Cat {
  name: string; // 这个是 this.name 的类型定义
  food: string;
  constructor(name: string, food: string) {
    this.name = name;
    this.food = food;
  }
  eat() {
    console.log(this.name + " eat " + this.food);
  }
}
let myCat: Cat = new Cat("Tom", fish);
console.log(myCat.eat());操作符
?.
可选链,在引用某个对象的属性时,会经常用到,在遇到 undefined 和 null 时会立即阻止表达式的运行,并返回 undefined,编译后的 es5 代码如下:
a?.b?.c
(_a = a === null || a === void 0 ? void 0 : a.b) === null || _a === void 0 ? void 0 : _a.c;void 运算符能对给定的表达式进行求值,然后返回undefined,在这里void 0就相当于undefined,为什么不直接用undefined呢?原因是undefined并不是保留字,它只是全局对象的一个属性,虽然在 es5 是全局对象的一个只读属性,但是在局部作用域中依然可以被重写。
??
更多人会接触到短路操作符 ||,常用于数据兜底,但是它会影响 falsy 值,包括”、0、false、undefined、null…,而 ?? 只会影响 undefined 和 null。?? 不能和 || 或者 && 并用,会报 SyntaxError 错误,但是可以用括号定义运算优先级来避免这种错误
!非空断言
可以用于忽略 undefined 和 null
function myFunc(id: string | undefined | null) {
  const myId: string = id;         // Error
  const ignoreUAndN: string = id!; // Ok
}
function myFunc(fn: Function | undefined) {
  fn!(); // Ok
}内置对象
object/Object/{}
- object - 表示非原始类型
- Object - 所有 Object 类的实例的类型
// node_modules/typescript/lib/lib.es5.d.ts
interface Object {
  constructor: Function;
  toString(): string;
  toLocaleString(): string;
  valueOf(): Object;
  hasOwnProperty(v: PropertyKey): boolean;
  isPrototypeOf(v: Object): boolean;
  propertyIsEnumerable(v: PropertyKey): boolean;
}
interface ObjectConstructor {
  // Object 类的所有实例都继承了 Object 接口中的所有属性
  new (value?: any): Object;
  (value?: any): any;
  readonly prototype: Object;
  getPrototypeOf(o: any): any;
  // ···
}
declare var Object: ObjectConstructor;- {} - 没有成员的对象
DOM
Document、HTMLElement、Event、NodeList
其他
Boolean、Error、Date、RegExp
ts.config.js
作用
- 用于标识 TypeScript 项目的根路径
- 用于配置 TypeScript 编译器
- 用于指定编译的文件
重要字段
- files - 设置要编译的文件的名称;
- include - 设置需要进行编译的文件,支持路径模式匹配;
- exclude - 设置无需进行编译的文件,支持路径模式匹配;
- 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          // 为装饰器提供元数据的支持
  }
}