学习【Typescript】基础

76 阅读15分钟

一、开始TypeScript

TypeScript是一门由微软推出的开源的、跨平台的编程语言。它是JavaScript的超集,扩展了 JavaScript 的语法,最终会被编译为JavaScript代码。

JavaScript是一种轻量级的解释性脚本语言。也是弱类型、动态类型语言,允许隐式转换,只有运行时才能确定变量的类型。正是因为在运行时才能确定变量的类型,JavaScript代码很多错误在运行时才能发现。TypeScript在JavaScript的基础上,包装了类型机制,使其变身成为静态类型语言。在 TypeScript 中,不仅可以轻易复用 JavaScript 的代码、最新特性,还能使用可选的静态类型进行检查报错,使得编写的代码更健壮、更易于维护。

1、环境

// npm
npm install -g typescript
// yarm
yarn global add typescript
// 查看版本
tsc -v

初始化配置文件tsc --init,生成 tsconfig.json 文件

{
  "compilerOptions": {
    "target": "es5",                        // 指定 ECMAScript 目标版本: 'ES5'
    "module": "commonjs",                   // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "moduleResolution": "node",             // 选择模块解析策略
    "experimentalDecorators": true,         // 启用实验性的ES装饰器
    "allowSyntheticDefaultImports": true,   // 允许从没有设置默认导出的模块中默认导入。
    "sourceMap": true,                      // 把 ts 文件编译成 js 文件的时候,同时生成对应的 map 文件
    "strict": true,                         // 启用所有严格类型检查选项
    "noImplicitAny": true,                  // 在表达式和声明上有隐含的 any类型时报错
    "alwaysStrict": true,                   // 以严格模式检查模块,并在每个文件里加入 'use strict'
    "declaration": true,                    // 生成相应的.d.ts文件
    "removeComments": true,                 // 删除编译后的所有的注释
    "noImplicitReturns": true,              // 不是函数的所有返回路径都有返回值时报错
    "importHelpers": true,                  // 从 tslib 导入辅助工具函数
    "lib": ["es6", "dom"],                  // 指定要包含在编译中的库文件
    "typeRoots": ["node_modules/@types"],
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": [                              // 需要编译的ts文件 *表示文件匹配 **表示忽略文件的深度问题
    "./src/**/*.ts"
  ],    
  "exclude": [                              // 不需要编译的ts文件
    "node_modules",
    "dist",
    "**/*.test.ts",
  ]
}

package.json 加入 script 命令

"build": "tsc",     // 执行编译
"build:w": "tsc -w" // 监听变化

2、配置 tslint

  • 安装:npm install tslint -g

  • 初始化:tslint -i

    多了一个 tslint.json 文件

    {
      "defaultSeverity": "error",
      "extends": [
        "tslint:recommended"
      ],
      "jsRules": {},
      "rules": {},
      "rulesDirectory": []
    }
    
    • defaultSeverity:提醒级别,如果为error则会报错,如果为warning则会警告,如果设为off则关闭,那TSLint就关闭了;
    • extends: 可指定继承指定的预设配置规则
    • jsRules: 用来配置对.js.jsx文件的校验,配置规则的方法和下面的rules一样
    • rules: TSLint检查代码的规则都是在这个里面进行配置,比如当我们不允许代码中使用eval方法时,就要在这里配置"no-eval": true
    • rulesDirectory: 可以指定规则配置文件,这里指定相对路径

二、基础数据类型

在 TypeScript 语法中,类型的标注主要通过类型后置语法来实现:“变量: 类型

1、number

TypeScript 和 JavaScript 一样,所有数字都是浮点数,所以只有一个 number 类型。

还支持 ES6 中新增的二进制和八进制字面量,所以 TypeScript 中共支持2、8、10和16这四种进制的数值:

let num: number;
num = 0b1111011; // 二进制的123
num = 0o173;     // 八进制的123
num = 0x7b;      // 十六进制的123

2、string

3、boolean

值只能是true或者false,赋值给布尔值的值也可以是一个计算之后结果为布尔值的表达式:

let bool: boolean = !!0
console.log(bool) // false

4、undefined 和 null

在 JavaScript 中,undefined和 null 是两个基本数据类型。在 TypeScript 中,这两者都有各自的类型,即 undefined 和 null,也就是说它们既是实际的值,也是类型。

let u: undefined = undefined;
let n: null = null;

第一行代码可能会报一个tslint的错误:Unnecessary initialization to 'undefined',就是不能给一个变量赋值为undefined。但实际上给变量赋值为undefined是完全可以的,所以如果想让代码合理化,可以配置tslint,将"no-unnecessary-initializer"设置为false即可。

默认情况下,undefined 和 null 是所有类型的子类型,可以赋值给任意类型的值,也就是说可以把 undefined 赋值给 void 类型,也可以赋值给 number 类型。当在 tsconfig.json 的"compilerOptions"里设置为 "strictNullChecks": true 时,就必须严格对待了。这时 undefined 和 null 将只能赋值给它们自身或者 void 类型。这样也可以规避一些错误。

5、bigInt

BigInt是ES6中新引入的数据类型,它是一种内置对象,它提供了一种方法来表示大于 (2^53 )- 1 的整数,BigInt可以表示任意大的整数。

6、symbol

symbol 是 ES6 新增的一种基本数据类型,它用来表示独一无二的值,可以通过 Symbol 构造函数生成。

const s = Symbol(); 
typeof s; // symbol

Symbol 前面不能加 new关键字,直接调用即可创建一个独一无二的 symbol 类型的值。

TypeScript 中还有一个 unique symbol 类型,它是symbol的子类型,这种类型的值只能由Symbol()Symbol.for()创建,或者通过指定类型来指定变量是这种类型。这种类型的值只能用于常量的定义和用于属性名。需要注意,定义unique symbol类型的值,必须用 const 而不能用let来声明。下面来看在TypeScript中使用Symbol值作为属性名的例子:

const key1: unique symbol = Symbol()
let key2: symbol = Symbol()
const obj = {
    [key1]: 'value1',
    [key2]: 'value2'
}
console.log(obj[key1]) // value1
console.log(obj[key2]) // error 类型“symbol”不能作为索引类型使用。
  • symbol 也可以作为属性名,因为symbol的值是独一无二的,所以当它作为属性名时,不会与其他任何属性名重复。当需要访问这个属性时,只能使用这个symbol值来访问(必须使用方括号形式来访问):
let name = Symbol(); 
let obj = { 
  [name]: "TypeScript" 
};
console.log(obj); // { Symbol(): 'TypeScript' }
​
console.log(obj[name]); // 'TypeScript' 
console.log(obj.name);  // undefined
  • 使用 Symbol 类型值作为属性名,这个属性是不会被 for…in遍历到的,也不会被 Object.keys() 、 Object.getOwnPropertyNames() 、 JSON.stringify() 等方法获取到:

    虽然这些方法都不能访问到Symbol类型的属性名,但是Symbol类型的属性并不是私有属性,可以使用 Object.getOwnPropertySymbols 方法获取对象的所有symbol类型的属性名

    const name = Symbol("name"); 
    const obj = { 
      [name]: "TypeScript", 
      age: 18 
    };
    const SymbolPropNames = Object.getOwnPropertySymbols(obj); 
    console.log(SymbolPropNames); // [ Symbol(name) ] 
    console.log(obj[SymbolPropNames[0]]); // 'TypeScript' 
    

    还可以使用ES6提供的 Reflect 对象的静态方法 Reflect.ownKeys ,它可以返回所有类型的属性名,Symbol 类型的也会返回:

    const name = Symbol("name"); 
    const obj = { 
      [name]: "TypeScript", 
      age: 18 
    };
    console.log(Reflect.ownKeys(obj)); // [ 'age', Symbol(name) ]
    
  • Symbol 包含两个静态方法, for 和 keyFor

三、复杂基础类型

除了JavaScript中的数组和对象,TypeScript中新增了元组、枚举、Any、void、never、unknown。

1、array

  • 直接定义: 通过 number[] 的形式来指定这个类型元素均为number类型的数组类型,推荐使用这种写法。
  • 数组泛型:通过 Array 的形式来定义,使用这种形式定义时,tslint 可能会警告让我们使用第一种形式定义,可以通过在tslint.json 的 rules 中加入 "array-type": [false] 就可以关闭 tslint 对这条的检测。

如果要指定一个数组里的元素既可以是数值也可以是字符串,那么可以使用这种方式: number|string[]

2、object

在TypeScript中,当想让一个变量或者函数的参数的类型是一个对象的形式时,可以使用这个类型

3、元祖

TypeScript 的元组类型,使得定义包含固定个数元素、每个元素类型未必相同的数组成为可能。

元组可以看做是数组的扩展,它表示已知元素数量和类型的数组,它特别适合用来实现多值返回。

确切的说,就是已知数组中每一个位置上的元素的类型,可以通过元组的索引为元素赋值

let arr: [string, number, boolean];

4、枚举

枚举类型使用enum来定义

enum Roles {
  SUPER_ADMIN = 0,
  ADMIN = 1,
  USER = 2 
}
​
const superAdmin = Roles.SUPER_ADMIN;
console.log(superAdmin); // 0
console.log(Roles[1])    // ADMIN

5、any

它是一个任意类型。any 类型会在对象的调用链中进行传导,即any 类型对象的任意属性的类型都是 any,

6、void

void 和 any 相反,any 是表示任意类型,而 void 是表示没有类型,就是什么类型都不是。这在定义函数,并且函数没有返回值时会用到

void 类型的变量只能赋值为 undefined 和 null ,其他类型不能赋值给 void 类型的变量。

7、never

never 类型指永远不存在值的类型,它是那些总会抛出异常根本不会有返回值的函数表达式的返回值类型,当变量被永不为真的类型保护所约束时,该变量也是 never 类型。

TypeScript使用never关键字来表示逻辑上不应该发生的情况和控制流

never 类型是任何类型的子类型,也可以赋值给任何类型;但是,没有类型是never的子类型或可以赋值给never类型(never 本身除外)。

也就是说,可以将never类型的变量分配给任何其他变量,但不能将其他变量分配给never。

  • 函数中never:使用 never 作为那些无法达到的终点的函数的返回值类型。函数抛出一个异常或者函数包含一个无限循环
  • 作为可变类型守卫:

8、unknown

TypeScript在3.0版本新增的类型,主要用来描述类型并不确定的变量。unknown类型的值不能随便操作

注意:

  • 任何类型的值都可以赋值给 unknown 类型
  • unknown 不可以赋值给其它类型,只能赋值给 unknown 和 any 类型
  • unknown 类型的值不能进行任何操作
  • 只能对 unknown 进行等或不等操作
  • unknown 类型的值不能访问其属性、作为函数调用和作为类创建实例

四、枚举

枚举就是一个字典,使用 enum 关键字定义,支持数字和字符串的枚举。

1、数字枚举

在仅指定常量命名的情况下,定义的就是一个默认从 0 开始递增的数字集合,称之为数字枚举。

  • 如果想要从其他值开始递增,可以将第一个值的索引值进行指定:
enum Color {
  Red = 2,
  Blue,
  Yellow
}
console.log(Color.Red, Color.Blue, Color.Yellow); // 2 3 4
  • 可以对一个字段指定一个索引值,那他后面没有指定索引值的就会依次加一
// 指定部分字段,其他使用默认递增索引
enum Status {
  Ok = 200,
  Created,
  Accepted,
  BadRequest = 400,
  Unauthorized
}
console.log(Status.Created, Status.Accepted, Status.Unauthorized); // 201 202 401
  • 还可以给每个字段指定不连续的任意索引值:
enum Status {
  Success = 200,
  NotFound = 404,
  Error = 500
}
console.log(Status.Success, Status.NotFound, Status.Error); // 200 404 500
  • 可以使用计算值和常量。但是要注意,如果某个字段使用了计算值或常量,那么该字段后面紧接着的字段必须设置初始值,这里不能使用默认的递增值了
// 初值为计算值
const getValue = () => {
  return 0;
};
enum RightIndex {
  a = getValue(),
  b = 1,
  c
}

2、字符串枚举

将定义值是字符串字面量的枚举称为字符串枚举,字符串枚举值要求每个字段的值都必须是字符串字面量,或者是该枚举值中另一个字符串枚举成员

// 使用枚举值中其他枚举成员
enum Message {
  Error = "error message",
  ServerError = Error,
  ClientError = Error
}
console.log(Message.Error); // 'error message'
console.log(Message.ServerError); // 'error message'

注意,这里的其他枚举成员指的是同一个枚举值中的枚举成员,因为字符串枚举不能使用常量或者计算值,所以不能使用其他枚举值中的成员。

3、反向映射

定义枚举类型的值时,可以通过 Enum['key'] 或者 Enum.key 的形式获取到对应的值 value。还支持反向映射,但是反向映射只支持数字枚举,不支持字符串枚举

enum Status {
  Success = 200,
  NotFound = 404,
  Error = 500
}
console.log(Status["Success"]); // 200
console.log(Status[200]); // 'Success'
console.log(Status[Status["Success"]]); // 'Success'

五、函数类型

1、函数类型定义

直接定义

一个函数的定义包括函数名、参数、逻辑和返回值。为函数定义类型时,完整的定义应该包括参数类型和返回值类型

function add(a: number, b: number): number {
  return a + b
}
// number是函数返回值类型,没有事void

接口定义

interface Add {
  (a: number, b: number): number;
}
let add: Add = (arg1: string, arg2: string): string => arg1 + arg2; 
// error 不能将类型“(arg1: string, arg2: string) => string”分配给类型“Add”

这个接口Add定义了这个结构是一个函数,两个参数类型都是number类型,返回值也是number类型。当指定变量add类型为Add时,再要给add赋值,就必须是一个函数,且参数类型和返回值类型都要满足接口Add

类型别名定义

type Add = (x: number, y: number) => number;
let add: Add = (arg1: string, arg2: string): string => arg1 + arg2; 
// error 不能将类型“(arg1: string, arg2: string) => string”分配给类型“Add”

使用type关键字可以给任何定义的类型起一个别名。上面定义了 Add 这个别名后,Add就成为了一个和(x: number, y: number) => number一致的类型定义。上面定义了Add类型,指定add类型为Add,但是给add赋的值并不满足Add类型要求,所以报错了。

这里的=>与 ES6 中箭头函数的=>不同。TypeScript 函数类型中的=>用来表示函数的定义,其左侧是函数的参数类型,右侧是函数的返回值类型;而 ES6 中的=>是函数的实现。

2、函数参数定义

可选参数

但有时候,函数有些参数不是必须的,我们就可以将函数的参数设置为可选参数。可选参数只需在参数名后跟随一个?

type Add = (x: number, y: number, z?: number) => number;
let add: Add = (arg1, arg2, arg3) => arg1 + arg2 + arg3;
add(1, 2);    // success   3
add(1, 2, 3); // success   6

需要注意,可选参数必须放在必选参数后面

默认参数

当为参数指定了默认参数时,TypeScript 会识别默认参数的类型;当调用函数时,如果给这个带默认值的参数传了别的类型的参数则会报错:

const add = (x: number, y = 2) => {
  return x + y;
};
add(1, "ts"); // error 类型"string"的参数不能赋给类型"number"的参数

注意:函数的默认参数类型必须是参数类型的子类型

const add = (x: number, y: number | string = 2) => {
  return x + y;
};

add 函数参数 y 的类型为可选的联合类型 number | string,但是因为默认参数数字类型是联合类型 number | string 的子类型,所以 TypeScript 也会检查通过。

剩余参数

const handleData = (arg1: number, ...args: number[]) => {};
handleData(1, "a"); // error 类型"string"的参数不能赋给类型"number"的参数

六、类类型

1、修饰符

  • public:修饰的是在任何地方可见、公有的属性或方法;
  • private:修饰的是仅在同一类中可见、私有的属性或方法;
  • protected:修饰的是仅在类自身及子类中可见、受保护的属性或方法。

protected还能用来修饰 constructor 构造函数,加了protected修饰符之后,这个类就不能再用来创建实例,只能被子类继承

  • readonly:类中可以使用readonly关键字将属性设置为只读

2、类的接口

类型接口

interface FoodInterface {
  type: string;
}
class FoodClass implements FoodInterface {
  // error Property 'type' is missing in type 'FoodClass' but required in type 'FoodInterface'
  static type: string;
  constructor() {}
}

接口 FoodInterface 要求使用该接口的值必须有一个 type 属性,定义的类 FoodClass 要使用接口,需要使用关键字implements

implements关键字用来指定一个类要继承的接口,如果是接口和接口、类和类直接的继承,使用extends,如果是类继承接口,则用implements。

接口检测的是使用该接口定义的类创建的实例,所以上面例子中虽然定义了静态属性 type,但静态属性不会添加到实例上,所以还是报错,

interface FoodInterface {
  type: string;
}
class FoodClass implements FoodInterface {
  constructor(public type: string) {}
}

也可以使用抽象类实现:

abstract class FoodAbstractClass {
  abstract type: string;
}
class Food extends FoodAbstractClass {
  constructor(public type: string) {
    super();
  }
}

泛型中使用类类型

const create = <T>(c: { new (): T }): T => {
  return new c();
};
class Info {
  age: number;
}
create(Info).age;
create(Info).name; // error 类型“Info”上不存在属性“name”

创建了一个 create 函数,传入的参数是一个类,返回的是一个类创建的实例

  • 参数 c 的类型定义中,new()代表调用类的构造函数,他的类型也就是类创建实例后的实例的类型。
  • return new c()这里使用传进来的类 c 创建一个实例并返回,返回的实例类型也就是函数的返回值类型。

所以通过这个定义,TypeScript 就知道,调用 create 函数,传入的和返回的值都应该是同一个类类型

七、接口类型

1、多余属性检查

interface Vegetables {
  color?: string;
  type: string;
}
​
const getVegetables = ({ color, type }: Vegetables) => {
  return `A ${color ? color + " " : ""}${type}`;
};
​
getVegetables({
  type: "tomato",
  size: "big"     // 'size'不在类型'Vegetables'中,会报错
});

使用类型断言

类型断言使用 as 关键字来定义

...
getVegetables({
  type: "tomato",
  size: 12,
  price: 1.2
} as Vegetables);

添加索引签名

interface Vegetables {
  color: string;
  type: string;
  [prop: string]: any;
}
....
getVegetables({
  color: "red",
  type: "tomato",
  size: 12,
  price: 1.2
});