TypeScript 基础(一)

一、环境搭建与编译执行

TypeScript 编写的程序并不能直接通过浏览器运行,我们需要先通过 TypeScript 编译器把 TypeScript 代码编译成 JavaScript 代码。

环境搭建

安装 node.js

TypeScript 的编译器是基于 Node.js 的,所以我们需要先安装 Node.js:node.js官网

安装完成以后,可以通过 终端 或者 cmd 等命令行工具来调用 node:

# 查看当前 node 版本
node -v
复制代码

安装 TypeScript 编译器

通过 NPM 包管理工具安装 TypeScript 编译器:

npm i -g typescript
// 或
yarn add -g typescript
复制代码

安装完成以后,我们可以通过命令 tsc 来调用编译器:

# 查看当前 tsc 编译器版本 
tsc -v
复制代码

win10 运行 tsc 报错

ts 编译报错

管理员身份运行 PowerShell, 设置 set-ExecutionPolicy RemoteSigned

微信图片_202105232310253.png

编译代码

默认情况下, TypeScript 的文件的后缀为 .ts

// ./src/test.ts
let str: string = 'una';

export {};
复制代码

TypeScript 与 ECMAScript 2015 一样,任何包含顶级 import 或者 export 的文件都被当成一个模块。相反地,如果一个文件不带有顶级的 import 或者 export 声明,那么它的内容被视为全局可见的

使用我们安装的 TypeScript 编译器 tsc.ts 文件进行编译:tsc ./src/test.ts默认情况下会在当前文件所在目录下生成同名的 js 文件

tsc ./src/test.ts
// 编译文件放在 src 文件夹
复制代码

image.png

常用编译选项

  • --outDir指定编译文件输出目录
tsc --outDir ./dist ./src/test.ts
// 编译文件放在dist文件夹下
复制代码

image.png

  • --target指定编译的代码版本目标,默认为 ES3
tsc --outDir ./dist --target ES6 ./src/test.ts
复制代码

image.png

  • --watch:在监听模式下运行,当文件发生改变的时候自动编译
tsc --outDir ./dist --target ES6 --watch ./src/test.ts
复制代码

通过上面几个例子,我们基本可以了解 tsc 的使用了,但是大家应该也发现了,如果每次编译都输入这么一大堆的选项其实是很繁琐的,好在 TypeScript 编译为我们提供了一个更加强大且方便的方式,编译配置文件:tsconfig.json ,我们可以把上面的编译选项保存到这个配置文件中。

编译配置文件

我们可以把编译的一些选项保存在一个指定的 json 文件中,默认情况下 tsc 命令运行的时候会自动
去加载运行命令所在的目录下的 tsconfig.json 文件,配置文件格式如下:

// ./tsconfig.json
{
    // 编译选项
    "compilerOptions": { 
        // js 输出目录
        "outDir": "./dist", 
        // 编译输出目标 ES 版本,默认ES3
        "target": "ES2015", 
        // 监听改动
        "watch": true, 
    }, 
    
    // 需要编译的模块
    // ** : 所有目录(包括子目录) 
    // * : 所有文件,也可以指定类型 *.ts 
    "include": ["./src/**/*"] // 相对路径
}
复制代码

有了单独的配置文件,我们就可以直接运行:tsc

image.png

指定加载的配置文件

// ./configs/tsconfig.json
{
    // 编译选项
    "compilerOptions": { 
        // js 输出目录
        "outDir": "../dist", 
        // 编译输出目标 ES 版本,默认ES3
        "target": "ES2015", 
        // 监听改动
        "watch": true, 
    }, 
    
    // 需要编译的模块
    // ** : 所有目录(包括子目录) 
    // * : 所有文件,也可以指定类型 *.ts 
    "include": ["../src/**/*"] // 相对路径
}
复制代码

删除 ./tsconfig.json 文件,重新创建新的配置文件,注意相对路径发生了改变。

使用 --project-p 指定配置文件目录,会默认加载该目录下的 tsconfig.json 文件

tsc -p ./configs
复制代码

也可以指定某个具体的配置文件,把配置文件的文件名改为ts.json

tsc -p ./configs/ts.json
复制代码

image.png

二、类型系统初始

类型系统组成

类型系统包含两个重要组成部分:

  • 类型标注(定义、注解) – typing
  • 类型检测(检查) – type-checking

类型标注

类型标注就是在代码中给数据(变量、函数(参数、返回值))添加类型说明,当一个变量或者函数(参数)等被标注后不能存储与标注类型不符合的类型

有了标注, TypeScript 编译器就能按照标注对这些数据进行类型合法检测,各种编辑器、IDE 等就能进行智能提示。

类型检测

顾名思义,就是对数据的类型进行检测。注意这里,重点是类型两字。

类型系统检测的是类型,不是具体值(虽然,某些时候也可以检测值),比如某个参数的取值范围(1-100之间),我们不能依靠类型系统来完成这个检测,它应该是我们的业务层具体逻辑,类型系统检测的是它的值类型是否为数字!

类型标注

在 TypeScript 中,类型标注的基本语法格式为:数据载体:类型

基础类型(string,number,boolean)

基础类型包含:stringnumberboolean

  • 布尔值 boolean

    let isDone: boolean = false;
    复制代码
  • 数字 number

    let decLiteral: number = 6;
    复制代码

    和 JavaScript 一样,TypeScript 里的所有数字都是浮点数。除了支持十进制和十六进制字面量,TypeScript 还支持 ECMAScript 2015 中引入的二进制和八进制字面量。

    let hexLiteral: number = 0xf00d;
    let binaryLiteral: number = 0b1010;
    let octalLiteral: number = 0o744;
    复制代码
  • 字符串 string let name: string = "bob";

    模版字符串:被反引号包围(`),以${ expr }形式嵌入表达。可以定义多行文本和内嵌表达式。

    let name: string = `una`;
    let age: number = 23;
    let sentence: string = `Hello, my name is ${ name }.
    
    I'll be ${ age + 1 } years old next month.`;
    复制代码

类型推断:变量初始化时如果没有标识,也会自动识别类型。

空(null)和未定义(undefined)类型

TypeScript 里,undefined 和 null 两者各自有自己的类型分别叫做 undefined 和 null 。这两种类型有且只有一个值,在标注一个变量为 Null 和 Undefined 类型,那就表示该变量不能修改了

let a: null; 
a = null; // ok 
a = 1; // error Null类型一旦标准不能修改
复制代码

默认情况下,null 和 undefined 是所有类型的子类型。 就是说你可以把 null 和 undefined 赋值给其它类型的变量

let a: number;
a = null; // ok 
复制代码

如果一个变量声明了,但是未赋值,那么该变量的值为 undefined ,但是如果它同时也没有标注类型的话,默认类型为 any

因为 nullundefined 都是其它类型的子类型,所以默认情况下会有一些隐藏的问题:

let a:number; 
a = null; // 提示:不能将类型'null'分配给类型'number'
a.toFixed(1); // ok(不报错,但实际运行是有问题的)
复制代码

然而,当你指定了 --strictNullChecks 标记,nullundefined 只能赋值给 void 和它们自己。 这能避免很多常见的问题。

// ./tsconfig.json
{
    "compilerOptions": {
        "outDir": "./dist",
        "target": "es5",
        "watch": true,
        "strictNullChecks": true // 严格的空值检查
    },
    "include": ["./src/**/*"]
}
复制代码
let a: number; 
a = null;
a.toFixed(1); // error 
复制代码

注意:TypeScript 鼓励尽可能地使用 --strictNullChecks

数组类型

TypeScript 中数组存储的类型必须一致,所以在标注数组类型的时候,同时要标注数组中存储的数据类型:

  • 泛型标注:Array<元素类型>

    // <number> 表示数组中存储的数据类型
    let arr1: Array<number> = []; 
    arr1.push(100); // ok 
    arr1.push('你好啊');// error 字符串不能赋给类型'number'的参数
    复制代码
  • 简单标注:在元素类型后面接上 [],表示由此类型元素组成的一个数组

    let arr2: string[] = []; 
    arr2.push('una'); // ok 
    arr2.push(1); // error 
    复制代码

元组类型

  • 元组表示一个 已知元素数量和类型的数组,各元素的类型不必相同
  • 初始化 数据的个数 以及 对应位置标注类型 必须一致
  • 当访问一个 越界(超过之前定义的数量) 的元素,会使用联合类型替代,但必须是元组标注中的类型之一
// 1. 定义一对值分别为 string 和 number 类型的元组
let x: [string, number]; // 声明一个元组类型
x = ['hello', 10]; // OK
x = [10, 'hello']; // Error 初始化 数据的个数 以及 对应位置标注类型 必须一致

// 2. 当访问一个已知索引的元素,会得到正确的类型
console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'

// 3. 当访问一个越界的元素,会使用联合类型替代:
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型
x[4] = true; // Error, 布尔不是(string | number)类型
复制代码

枚举类型(enum)

枚举的作用是 组织收集一组关联数据的方式,通过枚举我们可以给一组有关联意义的数据赋予一些友好的名字

enum HTTP_CODE { 
    ERROR,
    OK = 200,
    NOT_FOUND = 404, 
    METHOD_NOT_ALLOWED 
};

HTTP_CODE.ERROR;// 0 第一个枚举值默认为:0
HTTP_CODE.OK;// 200 
HTTP_CODE.METHOD_NOT_ALLOWED;  // 405 非第一个枚举值为上一个数字枚举值 + 1
HTTP_CODE.OK = 1; // error 枚举值为只读(常量),初始化后不可修改
复制代码

注意事项:

  • key 不能是数字

  • value 可以是数字,称为 数字类型枚举,也可以是字符串,称为 字符串类型枚举,但不能是其它值,默认为数字:0

  • 枚举值可以省略,如果省略,则:

    • 第一个枚举值默认为:0
    • 非第一个枚举值为上一个数字枚举值 + 1
  • 枚举值为只读(常量),初始化后不可修改

  • 如果前一个枚举值类型为字符串,则后续枚举项必须手动赋值

enum URLS { 
    USER_REGISETER = '/user/register', 
    USER_LOGIN = '/user/login',
    // 如果前一个枚举值类型为字符串,则后续枚举项必须手动赋值 
    INDEX = 0 
}
复制代码

枚举名称可以是大写,也可以是小写,推荐使用全大写(通常使用全大写的命名方式来标注值为常量)。

无值类型(void)

表示没有任何数据的类型,通常用于标注无返回值函数的返回值类型,函数默认标注类型为: void

function fn(): void { 
    // 没有 return 或者 return undefined 
}
复制代码

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

let unusable: void = undefined;
复制代码

strictNullChecks 为 false 的情况下, undefined 和 null 都可以赋值给 void ,但是strictNullCheckstrue 的情况下,只有 undefined 才可以赋值给 void

Never 类型

never 类型表示的是那些永不存在的值的类型:

  • 抛出异常
  • 无返回值的函数
  • 箭头函数
  • 永不为真的变量

never 类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是 never 的子类型,即使 any 也不可以赋值给 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) {
    }
}
复制代码

任意类型(any)

有的时候,我们并不确定这个值到底是什么类型 或者 不需要对该值进行类型检测,就可以标注为 any 类型:

let a: any;
复制代码
  • 一个变量 声明未赋值 且 未标注类型 的情况下,默认为 any 类型;
  • 任何类型值都可以赋值给 any 类型;
  • any 类型也可以赋值给任意类型;
  • any 类型有任意属性和方法;

标注为 any 类型,也意味着放弃对该值的类型检测,同时放弃 IDE 的智能提示

let a; // 一个变量 声明未赋值 且 未标注类型,默认为 any 类型。当 noImplicitAny 为 true 时,报错。
let c: any = 'una'; // any 类型是任何类型的子类型
let d: number = 1;
d.toFixed(1)

d = c; // 把 c 赋值给 d,d 变成 any 类型
d.toFixed(1); // d 变成字符串,不具备 toFixed 方法,虽然 IDE 没报错,但是一运行就会报错
复制代码

当指定 noImplicitAny 配置为 true ,当函数参数出现隐含的 any 类型时报错

未知类型(unknow)

unknow,3.0 版本中新增,属于安全版的 any,但是与 any 不同的是:

  • unknow 仅能赋值给 unknowany
  • unknow 没有任何属性和方法
let c: unknow = 'una'; 
let d: number = 1;
d.toFixed(1)

d = c; // error unknow 仅能赋值给 unknow、any 
d.toFixed(1); // error unknow 没有任何属性和方法
复制代码

对象类型

内置对象类型

在 JavaScript 中,有许多的内置对象,比如:ObjectArrayDate ……,我们可以通过对象的 构造函数 或者 来进行标注。

object 表示非原始类型,也就是除 numberstringbooleansymbolnullundefined 之外的类型。

let a: object = {}; 
let arr: Array<number> = [1,2,3]; 
let d1: Date = new Date();
复制代码

自定义对象类型

许多时候,我们可能需要自定义结构的对象。这个时候,我们可以通过以下方式来自定义对象:

  • 字面量标注
  • 接口
  • 定义 或者 构造函数
字面量标注
let a: {username: string; age: number} = { username: 'una', age: 18 };

a.username; // ok 
a.age; // ok 
a.gender;// error 
复制代码
  • 优点: 方便、直接;
  • 缺点: 不利于复用和维护
接口
interface Person { username: string; age: number; };
let a: Person = { username: 'una', age: 18 };

a.username; // ok 
a.age; // ok 
a.gender;// error 
复制代码
  • 优点: 复用性高;
  • 缺点: 接口只能作为类型标注使用,不能作为具体值,它只是一种抽象的结构定义,并不是实体,没有具体功能实现;
类 与 构造函数
class Person { 
    constructor(public username: string, public age: number) {
    
    } 
}

a.username; // ok 
a.age; // ok 
a.gender;// error 
复制代码
  • 优点: 功能相对强大,定义实体的同时也定义了对应的类型
  • 缺点: 复杂,比如只想约束某个函数接收的参数结构,没有必要去定一个类,使用接口会更加简单
interface AjaxOptions { 
    url: string; 
    method: string; 
}

function ajax(options: AjaxOptions) { } 

ajax({url: '', method: 'get' });
复制代码
包装对象

这里说的包装对象其实就是 JavaScript 中的 StringNumberBoolean,我们知道 string 类型 和 String 类型并不一样,在 TypeScript 中也是一样:

let a: string; // 简单类型字符串
a = '1'; 
a = new String('1'); // error String有的,string不一定有(对象有的,基础类型不一定有) 

let b: String; // 字符串对象
b = new String('2');
b = '2'; // ok 和上面正好相反 
复制代码

对象类型是可扩展的,但是简单类型不可扩展, string -> String 会丢失数据

函数类型

在 JavaScript 函数是非常重要的,在 TypeScript 也是如此。同样的,函数也有自己的类型标注格式:函数名称( 参数1: 类型, 参数2: 类型... ): 返回值类型;

function add(x: number, y: number): number { 
    return x + y; 
}
复制代码

类型断言:不再检查

有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”

类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。TypeScript会假设你已经进行了必须的检查。

类型断言有两种形式:

  • 尖括号语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length; // 尖括号语法:<string>someValue
复制代码
  • as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length; // as语法:someValue as string
复制代码

两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;然而,当你在 TypeScript 里使用 JSX 时,只有 as 语法断言是被允许的

三、接口详解

接口定义

接口:对复杂的对象类型进行标注的一种方式。

接口中多个属性之间可以使用 逗号 或者 分号 进行分隔。

接口的基础语法定义结构特别简单:

interface Point { x: number; y: number; }
复制代码

上面的代码定义了一个类型,该类型包含两个属性,一个 number 类型的 x 和一个 number 类型的
y。我们可以通过这个接口来给一个数据进行类型标注:

let p1: Point = { x: 100, y: 100 };
p1.x = 200; // ok
p1.z = 100; // error 类型“Point”上不存在属性“z”
复制代码

注意:接口是一种 类型 ,不能作为 使用。

interface Point { x: number; y: number; }
let p1 = Point; // error 接口是一种类型,不能作为值使用
复制代码

接口属性

可选属性

接口也可以定义可选的属性,通过 ? 来进行标注

interface Point { 
    x: number; 
    y: number; 
    color?: string; // 可选属性,等同于 string | undefined
}

let p1: Point = { x: 100, y: 100 }; // color 属性是可选的,没有也不报错
复制代码

优点:

  • 对可能存在的属性进行预定义
  • 可以捕获引用了不存在的属性时的错误
let p2: Point = { x: 100, y: 100, color: 'red' };
let p3: Point = { x: 100, y: 100, col: 'red' }; // error 类型“Point”上不存在属性“col”
复制代码

只读属性

我们还可以通过 readonly 来标注属性为只读,只读属性除了初始化以外,是不能被再次赋值的

interface Point { 
    readonly x: number; // 只读属性
    readonly y: number; 
}

let p1: Point = { x: 100, y: 100 };
p1.x = 200; // error 只读属性除了初始化以外,是不能被再次赋值的
复制代码

ReadonlyArray<T> 类型

TypeScript 还具有 ReadonlyArray<T> 类型,它与 Array<T> 相似,只是把所有可变方法去掉了,确保数组创建后再也不能被修改

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error 类型“readonly number[]”中的索引签名仅允许读取
ro.push(5); // error 类型“readonly number[]”上不存在属性“push”
ro.length = 100; // error 无法分配到 "length" ,因为它是只读属性
a = ro; // error 类型 "readonly number[]" 为 "readonly",不能分配给可变类型 "number[]"
复制代码

上面代码的最后一行,可以看到就算把整个ReadonlyArray赋值到一个普通数组也不可以。但是你可以用类型断言重写:

a = ro as number[]; // ok
复制代码

readonly vs const

最简单判断该用 readonly 还是 const 的方法是看要把它做为变量使用还是做为一个属性:变量 const,属性 readonly

任意属性(额外属性)

有的时候,我们希望给一个接口添加任意属性,可以通过索引类型来实现。

TypeScript 支持两种索引签名:字符串索引数字索引

数字索引类型:

interface Point { 
    x: number; 
    y: number; 
    [prop: number]: number; // 任意属性,数字索引
}
复制代码

字符串索引类型:

interface Point { 
    x: number; 
    y: number; 
    [prop: string]: number; // 任意属性,字符串索引
}
复制代码

注意:索引签名参数类型必须为 stringnumber 之一,但两者可同时出现

interface Point { 
    [prop1: string]: string; 
    [prop2: number]: string; 
}
复制代码

数字索引是字符串索引的子类型

注意:当同时存在数字类型索引和字符串类型索引时,数字索引的值类型 必须是 字符串索引的 值类型 或 子类型

这是因为当使用 number 来索引时,JavaScript 会将它转换成 string 然后再去索引对象。 也就是说用 100(number)去索引等同于使用”100″(string)去索引,因此两者需要保持一致。

interface Point1 { 
    [prop1: string]: string;
    [prop2: number]: number; // error 当 同时存在数字类型索引和字符串类型索引 时,数字类型的值类型必须是字符串类型的值类型或子类型
}

interface Point2 { 
    [prop1: string]: string;
    [prop2: number]: string; // ok
}

interface Point3 { 
    [prop1: string]: Object; 
    [prop2: number]: Date; // ok
}
复制代码

具体使用可看以下案例:

interface Point {
    readonly x: number;
    readonly y: number;
    color?: string; // 可选属性 string | undefined
    // [key: string]: number; // 这么设置的话 `color?: string` 会报错。
    // color 属性明显也符合 [key: string] 的设置,但是两者类型不同,一个是 string, 一个是 number
    [key: string]: number | string |undefined; // 兼容设置,扩展属性为3种类型即可
}

let p1: Point = {
    x: 100,
    y: 100
}
// p1.x = 200; // error 只读属性,仅能在初始化时赋值

p1.z = 100; // 扩展可选属性 [key: string]
p1[0] = 100; // 虽然0是number类型与可选属性设置不同,但是数字索引是字符串索引的子类型,所以不报错。但是反过来不行
// 等同于
// p1['0'] = 100;
复制代码

最后,你可以将索引签名设置为只读,这样就防止了给索引赋值:

interface ReadonlyStringArray {
    readonly [index: number]: string;
}

let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error 只读属性,仅能在初始化时赋值
复制代码

接口合并

多个同名的接口合并成一个接口

interface Box { 
    height: number; 
    width: number; 
}

interface Box { 
    scale: number; 
}

let box: Box = {height: 5, width: 6, scale: 10}
复制代码
  • 如果合并的接口存在同名非函数成员,则必须保证他们类型一致,否则编译报错;
  • 接口中的同名函数则是采用重载(后续会讲);

使用接口描述函数

我们还可以使用接口来描述一个函数

interface IFunc { 
    (a: string): string; // 单独描述函数,没有 key
}

let fn: IFunc = function(a) {}
复制代码

注意,如果使用接口来单独描述一个函数,是没 key 的

interface IEventFunc {
    (e: MouseEvent): void
}

function on(el: HTMLElement, evname: string, callback: IEventFunc) {
}

let div = document.querySelector('div');
if (div) {
    on(div, 'click', function(e) {
        e.clientX // HTMLElement 不存在 clientX 属性,MouseEvent 才存在 clientX 属性
    });
}
复制代码

使用接口描述类

与 C# 或 Java 里接口的基本作用一样,TypeScript 也能够用它来明确的强制一个类去符合某种契约,使用 implements

interface ClockInterface {
    currentTime: Date;
}

class Clock implements ClockInterface {
    currentTime: Date;
    constructor(h: number, m: number) { }
}
复制代码

接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。

具体讲解在 ts-笔记 面向对象编程 模块。

四、高级类型

联合类型

联合类型也可以称为多选类型,当我们希望标注一个变量为多个类型之一时可以选择联合类型标注()。

function css(ele: Element, attr: string, value: string | number) { 
    // ... 
}

let box = document.querySelector('.box'); 
// document.querySelector 方法返回值就是一个联合类型 

if (box) { // ts 会提示有 null 的可能性,加上判断更严谨 
    css(box, 'width', '100px'); 
    css(box, 'opacity', 1); 
    css(box, 'opacity', [1,2]); // 错误 
}
复制代码

交叉类型

交叉类型也可以称为合并类型,可以把多种类型合并到一起成为一种新的类型,对一个对象进行扩展():

interface o1 {
    x: number, 
    y: string
}; 

interface o2 {
    z: number
}; 

let o: o1 & o2 = Object.assign({}, {x: 1, y: '2'}, {z: 100});
console.log(o);
复制代码

小技巧:

TypeScript 在编译过程中只会转换语法(比如扩展运算符,箭头函数等语法进行转换,对于API 是不会进行转换的(也没必要转换,而是引入一些扩展库进行处理的),如果我们的代码中使用了 target 中没有的 API ,则需要手动进行引入,默认情况下 TypeScript 会根据target 载入核心的类型库

**target 为 es5 时: [“dom”, “es5”, “scripthost”] **

target 为 es6 时: [“dom”, “es6”, “dom.iterable”, “scripthost”]

如果代码中使用了这些默认载入库以外的代码,则可以通过 lib 选项来进行设置

www.typescriptlang.org/docs/handbo…

通过 lib 设置类型库

字面量类型

有的时候,我们希望标注的不是某个类型,而是一个固定值,就可以使用字面量类型,配合联合类型会更有用

function setPosition(ele: Element, direction: 'left' | 'top' | 'right' | 'bottom') {
    // ... 
}

box && setDirection(box, 'bottom'); // ok 
box && setDirection(box, 'hehe'); // error 
复制代码

类型别名

有的时候类型标注比较复杂,这个时候我们可以类型标注起一个相对简单的名字,我们称之为 类型别名

type dir = 'left' | 'top' | 'right' | 'bottom'; 
function setPosition(ele: Element, direction: dir) { 
    // ... 
}
复制代码

使用类型别名定义函数类型

这里需要注意一下,如果使用 type 来定义函数类型,和接口有点不太相同

// 使用类型别名定义函数类型
type callback = (a: string) => string; 
let fn: callback = function(a) {}; 

// 或者直接 
let fn: (a: string) => string = function(a) {}
复制代码

interface 与 type 的区别

  • interface
    • 只能描述 object / class / function 的类型
    • 同名 interface 自动合并,利于扩展
  • type
    • 能描述所有数据
    • 不能重名

类型推导

每次都显式标注类型会比较麻烦,TypeScript 提供了一种更加方便的特性:类型推导。TypeScript 编译器会根据当前上下文自动的推导出对应的类型标注,这个过程发生在:

  • 初始化变量
  • 设置函数默认参数值
  • 返回函数值
let x = 1; // 自动推断 x 为 number 
x = 'a'; // 不能将类型“"a"”分配给类型“number” 

// 函数参数类型、函数返回值会根据对应的默认值和返回值进行自动推断 
// 相当于 function fn(a?: number): number
function fn(a = 1) { return a * a }
复制代码

类型断言

有的时候,我们可能标注一个更加精确的类型(缩小类型标注范围),比如:

let img = document.querySelector('#img');
复制代码

我们可以看到 img 的类型为 Element,而 Element 类型其实只是元素类型的通用类型,如果我们去访问 src 这个属性是有问题的,我们需要把它的类型标注得更为精确:HTMLImageElement 类型,这个时候,我们就可以使用类型断言,它类似于一种 类型转换

let img = <HTMLImageElement>document.querySelector('#img');
// 或者
let img = document.querySelector('#img') as HTMLImageElement;
复制代码

注意:断言只是一种预判,并不会对数据本身产生实际的作用,即:类似转换,但并非真的转换了。

五、函数详解

一个函数的标注包含:

  • 参数
  • 返回值(如果函数没有返回任何值,你也必须指定返回值类型为 void 而不能留空)

对于返回值,我们在函数和返回值类型之前使用 => 符号,使之清晰明了。

// 1. 直接标注
function fn1(a: string): string {
    return '';
}

// 2. type 类型别名 标注函数
let fn2: (a: string) => string = function(a: string) {
    return '';
}

// 在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript编译器会自动识别出类型
let fn3: (a: string) => string = function(a) {
    return '';
}

// 3. type 类型别名 标注函数,拆分类型变量
type callback = (a: string) => string;

// 类型别名 使用的参数名为 a,函数使用的参数名为 b
// 只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确
let fn4: callback = function(b) {
    return ''
}

// 4. interface 接口标注函数
interface ICallBack {
    (a: string): string; // 使用接口单独描述函数,是没 key 的
}

let fn5: ICallBack = function(c) {
    return '';
}
复制代码

函数参数

可选参数

通过参数名后面添加 ? 来标注该参数是可选的。可选参数必须跟在必须参数后面。

let div = document.querySelector('div'); 
function css(el: HTMLElement, attr: string, val?: any) {}
// 错误写法:
// function css(el?: HTMLElement, attr: string, val: any) {}

div && css( div, 'width', '100px' ); // 设置 
div && css( div, 'width' ); // 获取 
复制代码

默认参数

我们还可以给参数设置默认值:

  • 有默认值的参数也是可选的
  • 设置了默认值的参数可以根据值自动推导类型

带默认值的参数不需要放在必须参数的后面。 如果带默认值的参数出现在必须参数前面,用户必须明确的传入 undefined 值来获得默认值

function sort1(items: Array<number>, order = 'desc') {} 

sort1([1,2,3]); 

// 也可以通过联合类型来限制取值,并设置默认值
// 'desc'|'asc' 联合类型,限制取值 
// = 'desc' 设置默认值
function sort2(items: Array<number>, order: 'desc'|'asc' = 'desc') {} 

sort2([1,2,3]); // ok 
sort2([1,2,3], 'asc'); // ok 
sort2([1,2,3], 'abc'); // error 

function sort3(order: 'desc'|'asc' = 'desc', items: Array<number>) {} 

sort3([1,2,3]); // error
sort3(undefined, [1,2,3]); // ok
sort3('asc', [1,2,3]); // ok
复制代码

剩余参数

剩余参数是一个数组,所以标注的时候一定要注意

interface IObj { 
    [key:string]: any; 
}

function merge(target: IObj, ...others: Array<IObj>) { 
    return others.reduce( (prev, currnet) => { 
        prev = Object.assign(prev, currnet); // 合并参数
        return prev; 
    }, target ); 
}

// target => {x: 1}, others => {y: 2}, {z: 3}
let newObj = merge({x: 1}, {y: 2}, {z: 3});
复制代码

函数中的this

普通函数

对于普通函数而言,this 是会随着调用环境的变化而变化的,所以默认情况下,普通函数中的 this 被标注为 any ,但我们可以在函数的第一个参数位(它不占据实际参数位置) 上显式的标注 this 的类型

interface T { 
    a: number; 
    fn: (x: number) => void; 
}

let obj1: T = { 
    a: 1, 
    fn(x: number) { 
        // 普调函数中的 this 为 any 类型 
        console.log(this); 
        // this.d // any 无检测,this.d 也不会报错
        (<T>this).d // 类型断言
    } 
}

let obj2: T = { 
    a: 1, 
    fn(this: T, x: number) { 
        // 通过第一个参数位标注 this 的类型,它对实际参数不会有影响
        console.log(this); // 有类型提示
    } 
}
obj2.fn(1);
复制代码

箭头函数

箭头函数的 this 不能像普通函数那样进行标注,它的 this 标注类型取决于它所在的作用域 this 的标注类型

interface T { 
    a: number; 
    fn: (x: number) => void; 
}

let obj2: T = { 
    a: 2, 
    fn(this: T) { // 箭头函数的this是固定的,取决于它所在作用域 this 的标注类型
        return () => { 
            // T 
            console.log(this); 
        } 
    } 
}
复制代码

函数重载

有的时候,同一个函数会接收不同类型的参数返回不同类型的返回值,我们可以使用函数重载来实现。

function showOrHide(ele: HTMLElement, attr: string, value: 'block' | 'none' | number) {}

let div = document.querySelector('div'); 
if (div) { 
    showOrHide( div, 'display', 'none' ); 
    showOrHide( div, 'opacity', 1 ); 
    
    // error,虽然通过了标注,但这里明显是有问题的
    // 虽然通过联合类型能够处理同时接收不同类型的参数,但是多个参数之间是一种组合的模式,我们需要的应该是一种对应的关系 
    showOrHide( div, 'display', 1 ); 
}
复制代码

函数重载:

  • 函数重载,是合并而不是覆盖
  • 通过函数重载可以设置不同的参数的对应关系
  • 重载函数类型只需要定义结构,不需要实体,类似接口
  • 查找重载列表,尝试使用第一个重载定义,如果匹配的话就使用该定义
// 通过函数重载可以设置不同的参数对应关系
// 重载函数类型只需要定义结构,不需要实体,类似接口
function showOrHide(ele: HTMLElement, attr: 'display', value: 'block' | 'none'); 
function showOrHide(ele: HTMLElement, attr: 'opacity', value: number); 
// 注意,function showOrHide(ele: HTMLElement, attr: string, value: any) 并不是重载列表的一部分,而是具体的函数实现。
// 这里只有两个重载,以其它参数调用 showOrHide 会产生错误。
function showOrHide(ele: HTMLElement, attr: string, value: any) { 
    ele.style[attr] = value; 
}

let div = document.querySelector('div'); 
if (div) { 
    showOrHide( div, 'display', 'none' ); 
    showOrHide( div, 'opacity', 1 ); 
    showOrHide( div, 'display', 1 );  // error
}
复制代码
interface PlainObject { 
    [key: string]: string | number; 
}

function css(ele: HTMLElement, attr: PlainObject); // 对象
function css(ele: HTMLElement, attr: string, value: string | number); // 字符串
function css(ele: HTMLElement, attr: any, value?: any) { 
    if (typeof attr === 'string' && value) { 
        ele.style[attr] = value; 
    }
    if (typeof attr === 'object') { 
        for (let key in attr) { 
            ele.style[attr] = attr[key]; 
        } 
    } 
}

let div = document.querySelector('div'); 
if (div) { 
    css(div, 'width', '100px'); 
    css(div, { width: '100px'});
    css(div, 'width'); // value? 如果不使用重载,这里就会有问题了 
}
复制代码

为了让编译器能够选择正确的检查类型,它与JavaScript里的处理流程相似。 它查找重载列表,尝试使用第一个重载定义,如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享