TypeScript最佳实践(一)TS基础

91 阅读12分钟

1. Typescript优势

优势1:编译时静态类型检测:函数或方法传参或变量赋值不匹配时,会出现编译错误提示,规避了开发期间的大量低级错误,省时,省力。

优势2:能自动提示:变量类型、变量属性,不用来回切换文件或不小心写错导致的编码隐患。

优势3: 引入了泛型:让大中项目,前端框架底层源码具备了高可扩展性这个巨大的优势,同时也有类型安全检查的优势。

优势4强大的 d.ts 声明文件:声明文件像一个书的目录一样,清晰直观展示了依赖库文件的接口,type类型,类,函数,变量等声明。

优势5:轻松编译成 JS 文件:即使 TS 文件有错误,绝大多数情况也能编译出 JS 文件。

优势6:灵活性高: 尽管 TS 是一门强类型检查语言,但也提供了 any 类型 和 as any 断言,这提供了TS的灵活度。

2. tsconfig.json常用配置

{
  "compilerOptions": {
    "target": "es2020", // 指定 TS 编译成 JS 后的js版本
    "module": "commonjs", // TS 编译成 JS 后采用的模块规范 commonjs amd cmd  es等         
    "lib": ["DOM","ES2020"], /*  指定 TS 编码期间可以使用的库文件版本 比如:ES5就不支持Set集合 */
    "outDir": "./dist", //     指定 TS 文件编译成 JS 后的输出目录                 /* Redirect output structure to the directory. */
    "rootDir": "./src", // 指定 TS 文件源码目录
    "strict": true, // 启用严格检查模式
    "strictNullChecks":false,// null 和 undefined即是值,也是类型, null 和 undefined 值 只能赋值给 any ,unknown和它们各自的类型
    "noImplicitAny": true, // 一般是指表达式或函数参数上有隐含的 any类型时报错
    "experimentalDecorators": true, /* 启用ES7装饰器实验开启选项 */
    "emitDecoratorMetadata": true, /* 启用装饰器元数据开启选项 */
    "declaration": true, // 指定 TS 文件编译后生成相应的.d.ts文件
    "removeComments": false, // TS 文件编译后删除所有的注释
    
    "baseUrl": "src", /* 工作根目录  解析非相对模块的基地址*/
    "paths": {
        "@/datatype/*": ["datatype/*"],
        "@/131/*": ["131/*"],
        "@/132/*": ["132/*"]
      },    
    // 有些依赖库底层 为了兼容CommonJs规范、AMD规范这二者的规范中相互兼容,
    // 使用了 export =,将二者规范统一。
    // "esModuleInterop":true表示允许依赖库中出现export = 这种兼容规范导出的格式,
    //  TS 可以用import from导入 
    "esModuleInterop": true,  
  },
  "include": [ // 需要编译的ts文件一个*表示文件匹配**表示忽略文件的深度问题
    "./src/**/*.ts" // 匹配src下所有的ts文件
, "src/datatype/typepsenumts"  ],
   "exclude": [
    "./src/**/test",
    "./src/**/premit", 
  ]
} 

3. TS类型

1. 布尔值 (boolean)

let isDone: boolean = false; 
// ES5:var isDone = false;

2. 数字 (number)

let decLiteral: number = 6;
// ES5:var decLiteral = 6;

3. 字符串 (string)

let name: string = 'bob';
// ES5:var name = 'bob';

4. 数组 (Array)

方法1: 在元素类型后面接上 []

let list: number[] = [1, 2, 3];
// ES5:var list = [1,2,3];

方法2: 使用数组泛型,Array<元素类型>

let list: Array<number> = [1, 2, 3]; // Array<number>泛型语法 
// ES5:var list = [1,2,3];

5. 元组 Tuple

let x: [string, number];
x = ['hello', 10];
// ES5:var x = ['hello', 10];

访问一个已知索引的元素,会得到正确的类型:

console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'

访问一个越界的元素,会使用联合类型替代:

x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型

console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString

x[6] = true; // Error, 布尔不是(string | number)类型

具名元组,可以为元组中的元素打上类似属性的标记

const arr: [name: string, age: number, male: boolean] = ['zhangsan', 18, true];

6. 枚举 (enum)

enum Color {Red, Green, Blue};
let c: Color = Color.Green;

默认情况下,从0开始为元素编号。 也可以手动的指定成员的数值。

enum Color {Red = 1, Green, Blue};
let c: Color = Color.Green;

转为ES5如下:

"use strict";
var Color; 
(function (Color) {
    Color[(Color["Red"] = 0)] = "Red";
    Color[(Color["Green"] = 1)] = "Green";
    Color[(Color["Blue"] = 2)] = "Blue"; 
})(Color || (Color = {})); 
var c = Color.Green;

常量枚举

常量枚举通过在枚举上使用 const 修饰符来定义,常量枚举不同于常规的枚举,他们会在编译阶段被删除。

常量枚举成员在使用的地方会被内联进来,之所以可以这么做是因为,常量枚举不允许包含计算成员。因此常量枚举会带来一个对性能的提升

const enum Color {Red, Green, Blue}
let c: Color = Color.Green;

转为es5:

var c = 1 /* Green */;

7. 空值 (void)

某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void

function warnUser(): void {
    console.log("This is my warning message");
}

声明一个void类型的变量没有什么大用,因为你只能为它赋予undefinednull

8. Null 和 Undefined 类型

默认情况下nullundefined是所有类型的子类型。 就是说你可以把 nullundefined赋值给number类型的变量。

然而,当你指定了--strictNullChecks标记,nullundefined只能赋值给void和它们各自。

9. Never

never类型表示的是那些永不存在的值的类型。例如, never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型;

// 返回never的函数必须存在无法达到的终点 
function error(message: string): never { 
    throw new Error(message); 
} 

// 推断的返回值类型为never 
function fail() { 
    return error("Something failed"); 
}

// 返回never的函数必须存在无法达到的终点 
function infiniteLoop(): never { 
    while (true) { } 
}

never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never

什么场景 never 能被直接推导出来而不用定义?

// dataFlowAnalysisWithNever 方法穷尽了 DataFlow 的所有可能类型。 
// 通过这个示例,我们可以得出一个结论:
// 使用 never 避免出现未来扩展新的类没有对应类型的实现,
// 目的就是写出类型绝对安全的代码。
type DataFlow = string | number
function dataFlowAnalysisWithNever(dataFlow: DataFlow) {
  if (typeof dataFlow === 'string') {
    console.log(dataFlow)
  } else if (typeof dataFlow === 'number') {

  } else {
    // dataFlow 在这里是 never 
    let nothings = dataFlow;//never
  }
}
dataFlowAnalysisWithNever("免税店")


export { }

10. 任意值 (any)

any是任何类型的父类型,任何类型都可以被归为 any 类型(Never除外)。

anyObject区别Object类型的变量只是允许你给它赋任意值,但是却不能够在它上面调用任意的方法,即便它真的有这些方法。

anyunknown区别:unknown 可以接收任何类型,但是无法赋值给其他类型(除 any 和 unknown 本身之外)

type T40 = keyof any; // string | number | symbol 
type T41 = keyof unknown; // never

11. unknown

顶级类型,它可以接收任何类型,但是无法赋值给其他类型(除 any 和 unknown 本身之外),因此在需要接收所有类型的场景下,优先考虑用 unknown 代替 any

对照于anyunknown是类型安全的。 任何值都可以赋给unknown,但是当没有类型断言或基于控制流的类型细化时unknown不可以赋值给其它类型,除了它自己和any外。 同样地,在unknown没有被断言或细化到一个确切类型之前,是不允许在其上进行任何操作的。

unknown联合类型

type T00 = unknown & null; // null 
type T01 = unknown & undefined; // undefined 
type T02 = unknown & null & undefined; // null & undefined (which becomes never) 
type T03 = unknown & string; // string 
type T04 = unknown & string[]; // string[] 
type T05 = unknown & unknown; // unknown 
type T06 = unknown & any; // any

unknown交叉类型

type T10 = unknown | null; // unknown 
type T11 = unknown | undefined; // unknown 
type T12 = unknown | null | undefined; // unknown 
type T13 = unknown | string; // unknown 
type T14 = unknown | string[]; // unknown 
type T15 = unknown | unknown; // unknown 
type T16 = unknown | any; // any

unknown不可以赋值给其它类型,除了它自己和any

let value: unknown;
let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error 
let value4: number = value; // Error 
let value5: string = value; // Error 
let value6: object = value; // Error 
let value7: any[] = value; // Error 
let value8: Function = value; // Error

不允许在其上进行任何操作

function f11(x: unknown) { 
    x.foo; // Error 
    x[5]; // Error 
    x(); // Error 
    new x(); // Error
}

12. object、Object、{}

1. Object

从JS沿袭过来,具有双重身份

  1. 第一种类型:为函数对象变量,可直接获取到属性和方法。
  2. 第二种类型:为实例类型,用来说明对象变量的类型 如:let obj:Object

重点说明:new Object() 中的 Object 依然是一个函数对象变量,不是函数类型,这一点容易混。

可以接受除了 null,undefined,unknown, void,never 五种数据类型之外的任何其他类型的数据,当我们 let obj:Object="abc", 等价于 let obj:Object=new String("abc"), 其他数据类型(Number、String等)亦同, 这符合 OOP 多态思想

let obj: Object = 1; // OK
obj = 'ww'; // OK
obj = {username: 'zzz'}; // OK
obj = ['z', 'y']; // OK
obj = undefined; // OK

2. {}

{} 描述一个没有成员的对象,试图访问它的任何属性时,TS都会编译错误。 但仍然可以访问 Object 类型上的所有属性/方法

const obj: {} = {};

// Error: Property 'prop' does not exist on type '{}'.
obj.prop = "semlinker";

obj.toString(); // "[object Object]"

虽然 Object{} 都可以接受基本类型的值,但并不包括 nullundefined

let obj: {} = {username: 'zzz'}
obj = true; // OK
obj = 124; // OK
obj = 'www'; // OK
obj = null; // Error
obj = undefined; // Error

可以明显感觉到,{ }Object 的效果几乎一样,即 {} == Object,但 Object 更规范

type t1 = object extends Object ? true : false; // true
type t2 = object extends {} ? true : false; // true

type t3 = {} extends Object ? true : false; // true
type t4 = Object extends {} ? true : false; // true

3. object

object表示对象类型,除了基础类型都可以给object赋值。

object 仅仅表示一种对象数据类型,不能直接拿object 或它的变量获取属性和方法,在 TS 中主要来接受以下四种类型的数据,本质上都是对象类型变量

// 1. 接受 type 类型的对象
type Door = { brand: string }
let door: Door = { brand: "喜盈门" }

// 2 接受类对象
class Customer {
    custname: string
    age: number
}
let cust: object = new Customer();

// 3 接受接口类型的对象
interface Animal {
    name: string
}
let animal: Animal = { name: "燕子" }

// 4.接受普通对象
let obj: object = {}
let o: object = {username: 'zzz'}; // OK
let o1: object = ['12', '33'];  // OK
obj = true; // Error
obj = 124; // Error
obj = 'www'; // Error
obj = null; // Error
obj = undefined; // Error

总结:

  • 在任何时候都不要,不要,不要使用 Object 以及类似的装箱类型。
  • 当你不确定某个变量的具体类型,但能确定它不是原始类型,可以使用 object。
  • 避免使用{}{}意味着任何非 null / undefined 的值,从这个层面上看,使用它和使用 any 一样恶劣。

4. 类型注解和类型推断

// 类型注解
let price: number = 3
type StudentType = { name: string, age: number }
let stuObj: StudentType = { name: "wangwu", age: 23 }

// 类型推导
let count = 3;
let custObj = { name: "wangwu", age: 23 }

5. 简单的取值为何总抛错

如何解决下面代码中obj[username]处的报错

let obj = { username: "wangwu", age: 23 }
let username = "username"

let result = obj[username];
let obj = { username: "wangwu", age: 23 }
// 方法1: 将let改为const, 因为let时username是可变的,改为const后,username类型就被确认为是'username'
const username = "username"
let result = obj[username];

let obj2: object = { username2: "wangwu", age: 23 }
const username2 = "username2"
// 方法2: 使用any断言
let result = (obj as any)[username2]

6. interface 和 type 区别

区别1: 定义类型范围不同

interface 只能定义对象类型或接口当名字的函数类型。

type 可以定义任何类型,包括基础类型、联合类型 ,交叉类型,元组。

// type 定义基础类型
type num=number 

//  type 定义联合类型例子1:
type baseType=string |number | symbol

//  type 定义联合类型例子2:
interface Car { brandNo: string}
interface Plane { No: string; brandNo: string}
type TypVechile = Car| Plane 

//  元组
interface Car { brandNo: string}
interface Plane { No: string; brandNo: string}
type TypVechile = [Car, Plane]

区别2 :接口可以 extends 一个或者多个接口或类, 也可以继承 type,但 type 类型没有继承功能,但一般接口继承类和 type 的应用场景很少见

区别3:用 type 交叉类可让类型中的成员合并成一个新的 type 类型,但接口不能交叉合并

type Group = { groupName: string, memberNum: number }
type GroupInfoLog = { info: string, happen: string }
type GroupMemeber = Group & GroupInfoLog// type 交叉类型合并

let data: GroupMemeber = {
  groupName: "001", memberNum: 10,
  info: "集体爬山", happen: "中途有组员差点滑落,有惊无险",
}

export { }

区别4:接口可以合并声明

定义两个相同名称的接口会合并声明,定义两个同名的type会出现编译错误。

interface Error {
  name: string;
}

interface Error {
  message: string;
  stack?: string;
}
// 接口合并
let error: Error = {
  message: "空指针",
  name: "NullPointException"
}

7. 声明文件

1. 为什么要用声明文件

如果文件使用 TS 编写,在编译时可以自动生成声明文件,并在发布的时候将 .d.ts 文件一起发布,我们无需编写声明文件。

当我们在 TS 文件中引入使用第三方库的类型或使用 集成库 时,比如:@types/jquery 库,ES6 库的 Map 类型 ,这些库用 JS 开发,不能获取 TS 一样的 类型提示,需要一个声明文件来帮助库的使用者来获取库的类型提示。

注意:声明文件中只对类型定义,不能进行赋值和实现。

2. 声明文件实现

// 关键字 declare 表示声明的意思,我们可以用它来做出各种声明:

declare let/const  // 声明全局变量
declare function   // 声明全局方法
declare class      // 声明全局类
declare enum       // 声明全局枚举类型 
declare namespace  // 声明(含有子属性的)全局对象
interface/type     // 声明全局类型

3. 构建时自动生成声明文件

declaration: true

8. 在 TS 中引入 js 文件

打开tsconfig.json中的allowJs: true

9. Number与number区别

自己理解:

class People {
    public myusername!: string;
    public myage!: number;
    public address!: string
    public phone: string
    constructor() {

    }
    eat() {

    }
    step() {
        console.log("People=>step");
    }
}

上面代码中People作为类型其实是People类的实例类型,同样的 new Number(1),这里Number也是Number类的实例类型,即number

let a: Number = Number(1);
let b: number = 2;
a = b; // OK
b = a; // Error

number可以赋给Number, Number不能赋给number