typescript基础

304 阅读25分钟

1.开发环境

  • Visual Studio默认包含了typescript,故用该编辑器不用再次安装
  • npm全局安装:npm install -g typescript
  • 检测有没有安装成功:执行命令 tsc -v,如果出现如下提示则安装成功

image.png

说明:typescript是用来编译ts的包,提供了tsc命令,实现了ts -> js的转化(可在终端中执行node xxx.js)。

编译文件的方式:

  1. 命令行手动编译ts文件:在终端将目录切到ts文件所在层级,执行 tsc xxx.ts,即可编译ts文件为js文件。

1650608879(1).png1650608964(1).png

  1. 自动编译ts文件:在终端将将路径切换到项目根目录,执行 tsc --init命令,生成 tsconfig.json配置文件,执行 tsc -w,启动监听模式,当ts文件发生改变的时候自动编译。

1650609128(1).png 1650609171(1).png 1650609294(1).png 1650609339(1).png

2.类型注解

为变量添加类型约束。

let age: number = 20; // number即类型注解,约定变量age的类型为number

3.常用基础类型

常用基础类型分为两类:1.js已有类型;2.ts新增类型。

js已有类型

  • 原始类型(即基本数据类型): Undefined、Null、Boolean、Number、String、Symbol(ES6)、BigInt(ES2020)。
  • 对象类型(即引用数据类型): Object(包含了function、Array、Date)。 ts新增类型
  • 联合类型、自定义类型(类型别名)、接口、元组、字面量类型、枚举、void、any等。
// 数组类型
let numbers: number[] = [1,3,5]; // 推荐写法
let strings: Array<string> = ['a','b','c'];

// 联合类型: 由两个或多个其他类型组成的类型,表示可以是这些类型中的任意一种。
let arr: (number | string)[] = [1,'a',3,'b']; // |(竖线)在ts中叫联合类型
let num: number | string[] = 1;
let arr2: number | string[] = ['a','b'];

/**
* 类型别名(自定义类型): 为任意类型起别名。
* 使用场景:当同一类型(复杂)被多次使用时,可以通过类型别名,简化该类型的使用。
* 1.使用type关键字创建类型别名;2.类型别名,可以是任意合法的变量名称;
* 3.创建类型别名后,直接使用该类型别名作为变量的类型注解即可。
*/
type CustomArray = (number | string)[]; //type为创建别名的关键字,CustomArray为类型别名
let arr1: CustomArray = [1,3,'a','b']; // 类型注解为CustomArray
let arr2: CustomArray = ['x','y',6,7];

/*
* 函数类型: 函数参数和返回值的类型。
* 指定类型有两种方式: 1.单独指定参数、返回值类型;2.同时指定参数、返回值的类型。
*
*/
// 单独指定参数、返回值类型
function add(num1: number, num2: number): number { // 函数声明
    return num1 + num2;
}
const add = (num1: number, num2: number): number => { // 函数表达式
    return num1 + num2;
}
// 同时指定参数、返回值的类型
const add: (num1: number, num2: number) => number = (num1, num2) => { // 函数表达式
    return num1 + num2; // 说明: 当函数作为表达式时,可以通过类似箭头函数形式的语法来为函
}                       //       数添加类型,这种形式只适用于函数表达式。

// void类型: 函数如果没有返回值,那么,函数返回值类型为 void。
function greet(name: string): void {
    console.log('hello', name);
}

// 元组类型: 是另一种类型的数组,它确切地知道包含多少个元素,以及特定索引对应的类型。
let data: [number, string, number] = [10, '8', 20]; // data的类型为元组,且有
                                                    // number,string,number类型
                                                    // 的三个元素。

/*
* 字面量类型:
* 任意的js字面量(例如, 对象、数字、字符串等)都可以作为类型使用。
* 使用模式: 通常字面量类型配合联合类型一起使用。
* 使用场景: 用来表示一组明确的可选值列表。
*/
const str = 'hello word'; // str的类型为 hello word,此处 hello word 就是一个字面量类型。
let a: 90 = 90; // a 的类型为 90, 此处 90 就是一个字面量类型

function changeDirection(direction: 'up' | 'down' | 'left' | 'right') {}
changeDirection('up'); // direction参数值只能为 'up','down','left','right'字面量类型之一。
                       // 优势: 相比于string类型,使用字面量类型更加精确、严谨。

/*
* 枚举类型: 定义一组命名常量。它描述一个值,该值可以是这些命名常量中的一个。
* 枚举是ts为数不多的非js类型级拓展(不仅仅是类型)的特性之一。
* 因为: 其他类型仅仅被当做类型,而枚举不仅用作类型,还提供值(枚举成员都是有值的)。
* 也就是说,其他成员会在编译为js代码时自动移除,但是,枚举类型会被编译为js代码。
* 枚举的功能类似于字面量类型+联合类型组合的功能,也可以表示一组明确的可选值。
* 一般情况下,推荐使用字面量类型+联合类型组合的方式,因为相比枚举,这种方式更加直观、简洁、高效。
* 说明: 1.使用enum关键字定义枚举;2.约定枚举名称、枚举中的值以大写字母开头;
* 3.枚举中多个值之间通过,(逗号)分隔;4.定义好枚举后,直接使用枚举名称作为类型注解。
*/
// 编译前:                    // 编译后:
enum Direction {             var Direction;
    Up = 'Up',               (function (Direction) {
    Down = 'Down',     =>        Direction["Up"] = "Up";
    Left = 'Left',               Direction["Down"] = "Down";
    Right = 'Right'              Direction["Left"] = "Left";
}                                Direction["Right"] = "Right";
                              })(Direction || (Direction = {}));

enum Direction { // 数字枚举
    Up, // 0 (枚举成员是有值的,默认为从0开始自增的数值)
    Down, // 1
    Left, // 2
    Right // 3
}
function changeDirection(direction: Direction) { } // 形参的类型为枚举,那么实参的值就
                                                   // 应该是枚举成员中的任意一个。
changeDirection(Direction.Up); // 类似于js中的对象,直接通过点(.)语法访问枚举的成员。

enum Direction { // 数字枚举(枚举成员的值是数字)
    Up, // 0 
    Down = 10, // 10
    Left, // 11
    Right // 12
}
enum Direction { // 字符串枚举(枚举成员的值是字符串)
    Up = 'Up', // 'Up'
    Down = 'Down', // 'Down' (字符串枚举没有自增长行为,因此,字符串枚举每个成员必须有初始值)
    Left = 'Left', // 'Left'
    Right = 'Right' // 'Right'
}
enum Direction { // 异构枚举(不建议这么做)
    Up = 'Up', // 'Up'
    Down = 1, // 1
    Left, // 2
    Right = 'Right' // 'Right'
}

4.函数

函数根据传参可以分为:默认参数、可选参数、剩余参数。

  • 默认参数
// 默认参数: 传入值会覆盖默认参数,也可不传值。
function getinfo(name: string, age: number = 20): string {
    return `${name}---${age}`;
}
console.log(getinfo("张三")); // 张三---20
console.log(getinfo("张三",30)); // 张三---30
  • 可选参数
/*
* 可选参数: 在可传可不传的参数名称后面添加?。
* 可选参数只能出现在参数列表最后,即可选参数后面不能再出现必选参数。
*/
function getinfo1(name: string, age?: number): string {
    if(age){
        return `${name}---${age}`;  
    }else{
        return `${name}---年龄保密`;
    }
}
console.log(getinfo1("张三")); // 张三---年龄保密
console.log(getinfo1("张三",30)); // 张三---30
  • 剩余参数
function sum(a: number, b: number, c: number, d: number): number {
    return  a + b + c + d;
}
console.log(sum(1, 2, 3, 4)); // 10
// => 可改写为剩余参数形式(相当于把参数赋值给一个数组,然后用循环遍历这个数组)
function sum1(...result: number[]): number {
    var sum = 0;
    for(let i=0; i<result.length; i++) {
        sum+= result[i];
    }
    return sum;
}
console.log(sum1(1, 2, 3, 4, 5)); // 15
console.log(sum1(1, 2, 3, 4, 5, 6)); // 21
// => 或改写为(把传进来的第一个参数赋值给a,后面的放进数组)
function sum1(a: number, ...result: number[]): number {
    var sum = a;
    for(let i=0; i<result.length; i++) {
        sum+= result[i];
    }
    return sum;
}
console.log(sum1(1, 2, 3, 4, 5)); // 15
console.log(sum1(1, 2, 3, 4, 5, 6)); // 21

5.对象

  • 对象类型: js中对象是由属性和方法构成的,而ts中对象的类型就是在描述对象的结构(有什么属性和方法)。
  • 可选属性: 对象的属性或方法,可以是可选的,此时就用到可选属性了。
/*
* 对象类型:
* 1.直接使用{}来描述对象结构。属性采用 属性名:类型 的形式,方法采用 方法名():返回值类型 的
* 形式。
* 2.如果方法有参数,就在方法名后面的小括号中指定参数类型(例如:greet(name: string): string)。
* 3.在一行代码中指定对象的多个属性时,使用;来分隔。
*  如果一行代码只指定一个属性类型(通过换行来分隔多个属性类型),可去掉;(分号)。
*  方法的类型也可以使用箭头函数形式(例如:{sayHi: () => void})。
*/
let person: { name: string; age: number; sayHi(): void } = { // 对象类型的写法
    name: '张三',
    age: 18,
    sayHi() {},
}
console.log(person); // {name: '张三', age: 18, sayHi: ƒ}

let person: { 
    name: string 
    age: number
    sayHi: () => void 
    greet: (name: string) => string
} = {
    name: '张三',
    age: 18,
    sayHi() {},
    greet(name) {
        return `hello ${name}`;
    }
}
console.log(person); // {name: '张三', age: 18, sayHi: ƒ, greet: ƒ}
console.log(person.greet('小明')); // hello 小明
// 对象可选属性:可选属性的语法与函数可选参数的语法一致,都使用?来表示。
function myAxios(config: { url: string; method?: string }) {}
myAxios({ url: '' });

6.接口

在面向对象的编程中,接口是一种规范的定义,它定义了行为和动作的规范,在程序设计里面,接口起到一种限制和规范的作用。接口定义了某一批类所需要遵守的规范,接口不关心这些类的内部状态数据,也不关心这些类里方法的实现细节,它只规定这批类里必须提供某些方法,提供这些方法的类就可以满足实际需要。 typescrip中的接口类似于java,同时还增加了更灵活的接口类型,包括属性、函数、可索引和类等!

typescript的接口分为:1.属性类型接口;2.继承接口;3.函数类型接口;4.可索引类型接口;5.类类型接口。

  • 属性类型接口: 对传入对象的约束。

    当一个属性类型(对象类型)被多次使用时,一般使用接口来描述对象的类型,达到复用的目的。

interface People {
    name: string;
    age: number;
}

function printPeople(personal: People) {
    console.log('姓名:'+personal.name+'年龄:'+personal.age); // 姓名:张三年龄:23
}
(function greet(): void {
    printPeople({ name: '张三', age: 23 })
})()

注意:属性类接口的属性另有 "可选属性" 与 "只读属性" 的特性。

interface People {
    name?: string; // 可选属性
    readonly age: number;
}

function printPeople(personal: People) {
    console.log('姓名:'+personal.name+'年龄:'+personal.age); // 姓名:undefined年龄:23
}
(function greet(): void {
    printPeople({ age: 23 })
})()
interface Point {
    x: number;
    readonly y: number; // 只读属性
}
(function greet(): void {
    let p1: Point = { x: 10, y: 20 }
    p1.y = 30; // error!
})()
  • 继承接口: 如果两个接口之间有相同的属性或方法,可以将公共的属性或方法抽离出来,通过继承来实现复用。
/*
* 1.使用extends关键字实现了接口Point3D继承Point2D
* 2.继承后Point3D就有了Point2D的所有属性和方法(此时,Point3D同时有x, y, z三个属性)。
*/
interface Point2D { x: number; y: number } // 这种方式很繁琐
interface Point3D { x: number; y: number; z: number } 
// => 更好的方式
interface Point2D { x: number; y: number }
interface Point3D extends Point2D { z: number }

let p3: Point3D = {
    x: 10,
    y: 0,
    z: 0,
}
  • 函数类型接口: 对方法传入的参数以及返回值进行约束。
interface Encrypt {
    (key:string, value:string): string;
}
let md5: Encrypt = function (key: string, value: string): string {
    return key + value;
}
let md5Str = md5('hello', 'word')
console.log(md5Str) // helloword

注意:1.函数的参数名不需要与接口里定义的名字相匹配;2.如果不想指定类型,TypeScript的类型系统会推断出参数类型,函数的返回值类型是通过其返回值推断出来。

interface Encrypt {
    (key:string, value:string): string;
}
let md5: Encrypt = function (key1, value1) {
    return key1 + value1;
}
let md5Str = md5('hello', 'word')
console.log(md5Str) // helloword
  • 可索引类型接口: 对数组和对象的约束(不常用)。
interface UserArr {
    [index: number]: string; // 对数组的约束
}
let arr: UserArr = ['89', 'hello word', 'a'];
let arr2: UserArr = ['89', 'hello word', 90]; // error
interface UserObj {
    [index: string]: string; // 对对象的约束
}

let obj: UserObj = { 'a': 'hello', 'b': 'word' };
let obj2: UserObj = []; // error
  • 类类型接口: 对类的约束和抽象类有点相似。
interface Animal {
    name: string;
    eat(str: string): void;
}

class Dog implements Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    eat() {
        console.log(this.name+'吃粮食') // 小黄吃粮食
    }
}
new Dog('小黄').eat();

7.接口与类型别名的区别

  • 相同点: 都可以给对象指定类型。
  • 不同点: 1.接口:只能为对象指定类型;2.类型别名:不仅可以为对象指定类型,实际上可以为任意类型指定别名。
interface IPerson {
    name: string
    age: number
    sayHi(): void
}
type IPerson = {
    name: string
    age: number
    sayHi(): void
}
type NumStr = number | string;

8.类型推论

  • 在ts中,某些没有明确指出类型的地方,ts的类型推论机制会帮助提供类型。
  • 换句话说:由于类型推论的存在,这些地方,类型注解可以省略不写。
  • 发生类型推论的2中常见场景: 1.声明变化并初始化时;2.决定函数返回值时。(这两种情况下,类型注解可以省略不写)
/*
* 类型注解:
* 推荐: 能省略类型注解的地方就省略(充分利用ts类型推论的能力,提高开发效率)。
*/
let age = 18; // age的类型为 number
function add(num1: number, num2: number) { // 返回值的类型为 number
    return num1 + num2;
}
let person = { // person的类型为 { name: string; age: number; }
    name: '张三',
    age: 18,
}
let b; // b的类型为 any (此处应该指定类型,例如: let b: numer; )
b = 10;
b ='hello word';

9.类型断言

有时候你会比ts更加明确一个值的类型,此时,可以使用类型断言来指定更具体的类型。

/*
* 类型断言:2种语法方式:1.值 as 类型; 2.<类型>值。
* 注意: getElementById方法返回的类型是 HTMLElement,该类型只包含所有标签公共的属性或方法,
* 不包含a标签特有的href等属性。
* 因此这个类型太宽泛(不具体),无法操作href等a标签特有的属性或方法。
* 解决方式: 这种情况下就需要使用类型断言指定更加具体的类型。
*/
<a href="xxx" id="link">demo</a>

const aLink = document.getElementById('link'); // aLink的类型为 HTMLElement
aLink.href // 此时ts会报错
// => 改为类型断言
const aLink = document.getElementById('link') as HTMLAnchorElement; // aLink的类型为 
aLink.href                                                          // HTMLAnchorElement
// 或
const aLink = <HTMLAnchorElement>document.getElementById('link');
aLink.href // 通过类型断言,aLink的类型变得更加具体,这样就可以访问a标签的属性或方法了。
interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}
function isFish(animal: Cat | Fish) {
    if (typeof (animal as Fish).swim === 'function') {
        return 'fish';
    }
    return false;
}
const cat: Cat = { name: '猫', run(){} }
const fish: Fish = { name: '金鱼', swim(){} }
console.log(isFish(cat)); // false
console.log(isFish(fish)); // fish

10.typeof 运算符

  • 在js中提供了typeof操作符,用来在js中获取数据的类型。
  • ts也提供了typeof操作符:可以在类型上下文中引用变量或属性的类型(类型查询)。 使用场景: 根据已有变量的值,获取该值的类型,来简化类型的书写。
/*
* typeof 出现在类型注解的位置(例如,参数名称、变量名称的冒号后面)所处的环境就在类型上下文
* (区别于js代码)。
*/
let p = { x: 1, y: 2 };
function formatPoint(point: typeof p) { } // 形参point的类型为 { x: number; y: number; }
formatPoint({ x: 1, y: 100 })

let num: typeof p.x; // num 的类型为 number

function add(num1: number, num2: number) { // add 方法的返回值类型为 number
    return num1 + num2;
}
let ret: typeof add(1, 2) // error (typeof 只能用来查询变量或属性的类型,无法查询其他形式
                          // 的类型(比如,函数调用的类型)。

11.类型兼容性

  • 两种类型系统: 1.Structural Type System(结构化类型系统); 2.Nominal Type System(标明类型系统)。
  • ts采用的是结构化类型系统, 也叫作duck typing(鸭子类型), 类型检查关注的是值所具有的形状。
  • 也就是说,在结构化类型系统中, 如果两个对象具有相同的形状,则认为它们属于同一类型。 类型兼容性主要有: 1.对象之间的类型兼容性;2.接口之间的类型兼容性;3.函数之间的类型兼容性。
/*
* 对象之间的类型兼容性:
*/

/*
* 1.Point与 Point2是两个名称不同的类; 2.变量 p 的类型被显示标注为 Point类型,但是,它的值
* 却是 Point2的实例,并且没有类型错误; 3.因为 ts是结构化类型系统,只检查 Point与 Point2的
* 结构是否相同(相同,都具有 x 和 y 两个属性,属性类型也相同); 4.但是,如果在 Nominal Type 
* System 中(例如, c#, java等), 它们是不同的类,类型 无法兼容。
*/
class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}
class Point2 {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}
const p: Point = new Point2(1, 3);
-----------------------------------------------------------
/*
* 注意: 在结构化类型系统中,如果两个对象具有相同形状,则认为它们属于同一类型,这种说法并不准确。
* 更准确的说法: 对于对象类型来说, y 的成员至少与 x 相同,则 x 兼容 y (成员多的可以赋值给成员
* 少的)。
* 1.Point2的成员至少与 Point相同,则 Point兼容Point2; 2.所以成员多的 Point2可以赋值给成员
* 少的 Point。
*/
class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}
class Point2 {
    x: number;
    y: number;
    z: number;
    constructor(x: number, y: number, z: number) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}
class Point3 {
    x: number;
    y: number;
    z: number;
    constructor(x: number, y: number, z: number) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
    changeX(val: number): void {
        this.x = val;
    }
}
const p: Point = new Point2(1, 3, 5);
const p2: Point2 = new Point(1, 3); // error (类型Point中缺少属性z,但类型Point2中需要
                                    // 该属性)。
const p3: Point2 = new Point3(1, 3, 5);
const p4: Point = new Point3(1, 3, 5);
const p5: Point3 = new Point(1, 3); // error (类型Point缺少类型Point3中的以下属性: z, 
                                    // changeX)。

/*
* 接口之间的类型兼容性:
*/
interface Point {
    x: number;
    y: number;
}
interface Point2 {
    x: number;
    y: number;
}
interface Point3 {
    x: number;
    y: number;
    z: number;
}
let p1: Point = { x: 1, y: 3 };
let p2: Point2 = p1;
let p3: Point3 = { x: 1, y: 3, z: 5 };
p2 = p3; // 接口之间的兼容性类型于 class
p3 = p2; // error (类型 Point2 中缺少属性 z,但类型 Point3 中需要该属性)。
-----------------------------------------------------------
interface Point2 {
    x: number;
    y: number;
}
class Point3 {
    x: number;
    y: number;
    z: number;
    constructor(x: number, y: number, z: number) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

let p3: Point2 = new Point3(1, 3, 5); // class 和 interface 之间也可以兼容

/*
* 函数之间的类型兼容性:
* 函数之间类型兼容性比较复杂,需要考虑: 1.参数个数; 2.参数类型; 3.返回值类型。 
* 参数类型: 相同位置的参数类型要相同(原始类型)或兼容(对象类型)。
* 返回值类型: 只关注返回值类型本身即可。
*/
type F1 = (a: number) => void;
type F2 = (a: number, b: number) => void;

let f1: F1 = function(a) {};
let f2: F2 = function(a, b) {}
let f3: F2 = function(a) {}
let f4: F2 = function(a, b, c) {} // error [不能将类型(a: any, b: any, c: any) => void
                                  // 分配给类型F2]。
f2 = f1; // 参数多的兼容参数少的 (参数少的可以赋值给参数多的, 所以, f1可以赋值给f2)。
f1 = f2; // error (不能将类型F2分配给类型F1)。
f2 = f3;
f3 = f2;
-----------------------------------------------------------
const arr = ['a', 'b', 'c'];

arr.forEach(() => {}); // 数组forEach方法的第一个参数是回调函数,该实例中的类型为
                       // (value: string, index: number, array: string[]) => void。
arr.forEach((item) => {});
arr.forEach((item, index) => {});
// 说明: 在ts中省略用不到的函数参数实际上是很常见的,这样的使用方式,促成了ts中函数类型之间
// 的兼容性; 并且因为回调函数是有类型的,所以,ts会自动推导出参数item,index,array的类型。
-----------------------------------------------------------
interface Point {
    x: number;
    y: number;
}
interface Point2 {
    x: number;
    y: number;
    z: number;
}

type F = (p: Point) => void;
type F2 = (p: Point2) => void;
let f: F = function({x, y}) {};
let f2: F2 = function({x, y}) {};
let f3: F2 = function({x, y, z}) {};
let f4: F2 = function({x, y, z, j}) {}; // error (类型Point2上不存在属性j)。
f = f2; // error (不能将类型F2分配给F1, 类型Point中缺少属性z, 但类型Point2中需要该属性)。
f2 = f; // 参数少的 f 可以赋值给参数多的 f2
f2 = f3;
f3 = f2;
-----------------------------------------------------------
type F5 = () => string;
type F6 = () => string;

let f5: F5 = function(): string { return '' };
let f6: F6 = function(): string { return '' };
f5 = f6; // 如果返回值类型是原始类型,此时两个类型要相同,比如 F5 和 F6
f6 = f5;

type F7 = () => { name: string };
type F8 = () => { name: string, age: number };

let f7: F7 = function() { return { name: '' } };
let f8: F8 = function() { return { name: '', age: 18 } };
f7 = f8; // 如果返回值类型是对象类型,此时成员多的可以赋值给成员少的,比如 F7 和 F8。
f8 = f7; // error [不能将类型F7分配给类型F8,类型{ name: string; }中缺少属性age,但类型
         // { name: string; age: number; }中需要该属性]。

12.交叉类型

  • 交差类型(&): 功能类似于接口继承(extends),用于组合多个类型为一个类型(常用于对象类型)。
  • 交叉类型和接口继承的对比: 1.相同点: 都可以实现对象类型的组合;2.不同点: 两种方式实现类型组合时,对于同名属性之间,处理类型冲突的方式不同。
interface Person {
    name: string;
    say(): number;
}
interface Concat {
    phone: string;
}

type PersonDetail = Person & Concat; // 使用交叉类型后,新的类型 PersonDetail 同时具备了
                                     // Person 和 Concat的所有属性类型。
// 相当于:
type PersonDetail = { name: string; phone: string; say(): number; }

let obj: PersonDetail = {
    name: '张三',
    phone: '135...',
    say() { return 1 }
}
-----------------------------------------------------------
interface A {
    fn: (value: number) => string
}
interface B extends A { // error (属性 fn 的类型不兼容)。
    fn: (value: string) => string
}

interface A {
    fn: (value: number) => string
}
interface B {
    fn: (value: string) => string
}
type C = A & B; // 属性 fn 可以简单的理解为 fn: (value: number | string) => string。
let c: C = {
    fn(value: number | string) {
        return '';
    }
}

13.泛型

软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。这样用户就可以自己的数据类型来使用组件。

通俗理解:泛型就是解决 类 接口 方法的复用性、以及对不特定数据类型的支持。 说明:泛型可以支持不特定的数据类型,传入的参数和返回的参数一致。

泛型应用:1.泛型函数;2.泛型类;3.泛型接口。

  • 泛型函数
/*
* 泛型函数: 
* 推荐: 使用简化的方式调用泛型函数,使代码更简洁,更易于阅读。
* 说明: 当编译器无法推断类型或者推断的类型不准确时,就需要显式地传入类型参数。
*/
function getData<T>(value: T): T {
    return value;
}
console.log(getData<number>(90)); // 90
console.log(getData('张三')); // 张三 (简化调用泛型函数: 1.在调用泛型函数时,可以省略<类型>
                              // 来简化泛型函数的调用; 2.此时,ts内部会采用一种叫做类型参
                              // 数推断的机制,来根据传入的实参自动推断出类型变量T的类型)。
console.log(getData<'李四'>('李四')); // 李四
console.log(getData<number>('90')); // error
  • 泛型类
class MinClass<T> {
    public list: T[] = [];
    add(value: T): void {
        this.list.push(value);
    }
    min(): T {
        let minNum = this.list[0];
        for(let i=0; i<this.list.length; i++) {
            if(minNum > this.list[i]) {
                minNum = this.list[i];
            }
        }
        return minNum;
    }
}
let m1 = new MinClass<number>();
m1.add(15);
m1.add(10);
m1.add(20);
console.log(m1.min()); // 10
  • 泛型接口
// 第一种写法
interface ConfigFn {
    <T>(value: T): T;
}

let getData: ConfigFn = function<T>(value: T): T {
    return value;
}
console.log(getData<string>('张三')); // 张三

// 第二种写法
interface ConfigFn<T> {
    (value: T): T;
}
function getData<T>(value: T): T {
    return value;
}
let myGetData: ConfigFn<string> = getData;
console.log(myGetData('张三')); // 张三
-----------------------------------------------------------
interface IdFunc<Type> {
    id: (value: Type) => Type;
    ids: () => Type[];
}

let obj: IdFunc<number> = {
    id(value) { return value },
    ids() { return [1, 3, 5] }
}
console.log(obj.ids()); // [1, 3, 5]
  • 把类作为参数类型的泛型类
class User {
    username: string | undefined;
    password: string | undefined;
}
class MysqlDb<T> {
    add(info: T): boolean {
        console.log(info); // User {username: '张三', password: '123'}
        return true;
    }
}
let u = new User();
u.username = '张三';
u.password = '123';
let db = new MysqlDb<User>();
db.add(u);
  • 泛型工具类型
/*
* 泛型工具类型: ts内置了一些常用的工具类型,来简化ts中的一些常见操作。
* 他们都是基于泛型实现的(泛型适用于多种类型,更加通用),并且是内置的,可以直接在代码中使用。
* 这些工具类型有很多, 主要学习这几个: Partial<Type>、Readonly<Type>、Pick<Type>、
* Record<Keys, Type>。
*/
interface Props {
    id: string;
    children: number[];
}
type PartialProps = Partial<Props>; // 构造出来的新类型 PartialProps 结构和 Props 相同,
                                    // 但所有的属性变为可选的。[Partial<Type>用来构造
let p1: PartialProps = {            // (创建)一个类型,将Type的所有属性设置为可选]。
    id: '张三'                      
}
let p2: PartialProps = {
    id: '张三',
    children: [1, 3, 5]
}
-----------------------------------------------------------
interface Props {
    id: string;
    children: number[];
}
type ReadonlyProps = Readonly<Props>; // 构造出来的新类型ReadonlyProps结构和Props相同,
let p1: ReadonlyProps = {             // 但所有属性都变为只读的。[Readonly<Type>用来构造
    id: '张三',                       // 一个类型,将Type的所有属性设置为readonly(只读)]。
    children: [1, 3, 5]
}
p1.id = "李四"; // error (无法分配到 "id" ,因为它是只读属性)。
-----------------------------------------------------------
interface Props {
    id: string;
    title: string;
    children: number[];
}
type PickProps = Pick<Props, 'id' | 'title'>; // 构造出来的新类型PickProps,只有id和
                                              // title两个属性类型。[Pick<Type, Keys>
let p: PickProps = {                          // 从Type中选择一组属性来构造新类型, Pick
    id: '张三',                               // 工具类型有两个类型变量: 1.表示选择谁的
    title: '老张'                             // 属性;2.表示选择哪几个属性。其中第二个类
}                                             // 型变量,如果只选择一个则只传入该属性名即
                                              // 可。第二个类型变量传入的属性只能是第一个
                                              // 类型变量中存在的属性]。
-----------------------------------------------------------
type RecordObj = Record<'a' | 'b' | 'c', string[]>; // 构建的新对象类型 RecordObj表示:
let obj: RecordObj = {                              // 这个对象有三个属性,分别为 a、b、c
    a: ['x', 'y'],                                  // ,属性值的类型都是 string[]。
    b: ['张三'],                                    // (Record<Keys, Type>构造一个对象
                                                    // 类型,属性键为 Keys,属性类型为Type
    c: ['李四']                                     // )。
}
  • 泛型约束
/*
* 泛型约束: 
* 默认情况下,泛型函数的类型变量Type可以代表多个类型,这导致无法访问任何属性,此时,就需要为
* 泛型添加约束来收缩类型(缩窄类型取值范围)。
* 添加泛型约束收缩类型,主要有两种方式: 1.指定更加具体的类型; 2.添加约束。
*/
function id<Type>(value: Type[]) { // 指定更加具体的类型 Type[],因为,只要是数组就一定
    return value.length;           // 存在length属性,因此就可以访问了。    
}
-----------------------------------------------------------
interface Length { // 创建描述约束的接口 Length,该接口要求提供length属性
    length: number;
}

function id<Type extends Length>(value: Type): number { // 通过extends关键字使用该接口
    return value.length;                                // ,为泛型(类型变量)添加约束。
}                                                       // 该约束表示: 传入的类型必须具
                                                        // 有length属性。
let a = id(['a', 'b']); // 数组有length属性
let b = id(90); // error (number类型的数值没有length属性)。
let c = id('hello word'); // 字符串有length属性
let d = id({ length: 1, name: '张三' }); // 对象中存在一个满足类型Length中length属性类型
                                         // 的属性亦可 (满足类型Length中length属性约束)。
// 注意: 传入的实参(例如, 数组)只要有length属性即可。
-----------------------------------------------------------
/*
* 泛型的类型变量可以有多个,并且类型变量之间可以约束(比如,第二个类型变量受第一类型变量约束)。
*/
/*
* 例如如下示例:
* 1.添加了第二个类型变量key,两个类型变量之间使用,(逗号)分隔;
* 2.keyof关键字接收一个对象类型,生成其键名称(可能是字符串或数字)的联合类型;
* 3.如下变量prop中, keyof Type实际上获取的是{ name: '张三', age: 18 }对象所有键的联合类型,
* 也就是: 'name' | 'age';
* 4.类型变量Type受Key约束,可以理解为:Key只能是Type所有键中的任意一个,或者说只能访问对象中存
* 在的属性。
*/
function getProps<Type, Key extends keyof Type>(obj: Type, key: Key) {
    return obj[key];
}

let prop = getProps({ name: '张三', age: 18 }, 'name');
let prop2 = getProps({ name: '张三', age: 18 }, 'name2'); // error (类型'name2'的参数
let prop3 = getProps(18, 'toFixed');                     // 不能赋给类型 name | age的
let prop4 = getProps('abc', 'split');                    // 参数)。
let prop5 = getProps('abc', 1); // 1 表示索引
let prop6 = getProps(['a'], 'length');
let prop7 = getProps(['x', 'y', 'z'], 1);

console.log(prop); // 张三
console.log(prop5); // b
console.log(prop6); // 1
console.log(prop7); // y                                        

14.索引签名类型

  • ts中,绝大多数情况下,我们都可以在使用对象前就确定对象的结构,并为对象添加准确的类型。
  • 使用场景: 当无法确定对象中有哪些类型(或者说对象中可以出现任意多个类型),此时,就用到索引签名类型了。
interface AnyObject {
    [key: string]: number; // 1.使用[key: string]来约束该接口中允许出现的属性名称。表示只
}                          // 要是string类型的属性名称,都可以出现在对象中; 2.key只是一个
                           // 占位符,可以换成任意合法的变量名称。
let obj: AnyObject = { // 对象obj中可以出现任意多个属性(比如a、'b'等)。
    a: 1,
    'b': 3
}

/*
* 在js中数组是一类特殊的对象, 特殊在数组的键(索引)是数值类型。
* 并且, 数组也可以出现任意多个元素。所以, 在数组对应的泛型接口中, 也用到了索引签名类型。
*/
interface MyArray<Type> {  // MyArray接口模拟原生的数组接口,并使用[index: number]来作为
    [index: number]: Type; // 索引签名类型; 该索引签名类型表示: 只要是number类型的键(索引)
}                          // 都可以出现在数组中, 或者说数组中可以有任意多个元素; 同时也
                           // 符合数组索引是number类型这一前提。
let arr: MyArray<number> = [1, 3, 5];

15.映射类型

  • 映射类型: 基于旧类型创建新类型(对象类型),减少重复、提升开发效率。
type Type1 = { x: number; y: number; z: number };

/*
* 映射类型写法: 
* 1.映射类型是基于索引签名类型的,所以,该语法类似于索引签名类型,也使用了[];
* 2.Key in PropKeys表示Key可以是PropKeys联合类型中的任意一个,类似于forin(let key in obj);
* 3.使用映射类型创建的新对象类型Type2和类型Type1结构完全相同;
* 4.注意: 映射类型只能在类型别名中使用,不能在接口中使用。
*/
type PropKeys = 'x' | 'y' | 'z';
type Type2 = { [Key in PropKeys]: number }; // { x: number; y: number; z: number; }
-----------------------------------------------------------
/*
* 映射类型除了根据联合类型创建新类型外, 还可以根据对象类型来创建:
* 1.首先,先执行 keyof Props获取到对象类型Props中所有键的联合类型,即 'a' | 'b' | 'c';
* 2.然后 Key in...就表示 Key可以是 Props中所有的键名称中的任意一个。
*/
type Props = { a: number; b: number; c: boolean };
type Type3 = { [Key in keyof Props]: number }; // { a: number; b: number; c: number; }
  • 分析泛型工具类型Partial的实现
/*
* 实际上,泛型工具类型(例如, Partial<Type>)都是基于映射类型实现的。
* Partial<Type>:
* 1.keyof T即 keyof Props表示获取Props的所有键,也就是: 'a' | 'b' | 'c';
* 2.在[]后面添加?(问号),表示将这些属性变为可选的,以此来实现Partial的功能;
* 3.冒号后面的T[P]表示获取T中每个键对应的类型,比如,如果是'a'则类型是number,如果是'b'则类型
* 是string;
* 4.最终,新类型PartialProps和旧类型Props结构完全相同,只是让所有类型变为可选了。
*/
type Partial<T> { // Partial<Type>的源码实现
    [P in keyof T]?: T[P];
}

type Props = { a: number; b: string; c: boolean };
type PartialProps = Partial<Props>; // { a?: number | undefined; b?: string | 
                                    // undefined; c?: boolean | undefined; }
  • 索引查询类型 在ts中,索引查询(访问)类型,用来查询属性的类型。

语法结构: T[P]。

type Props = { a: number; b: string; c: boolean; };
type MyPartial<T> = { // 模拟 Partial类型
    [P in keyof T]: T[P];
}

type TypeA = Props['a']; // number (表示查询类型 Props中属性'a'对应的类型number)
type TypeA = Props['a1']; // error (类型 Props 上不存在属性 a1)。
type PartialProps = MyPartial<Props>; // { a: number; b: string; c: boolean; }
// 注意: []中的属性必须存在于被查询类型中,否则就会报错。
-----------------------------------------------------------
/*
* 索引查询类型的其他使用方式: 同时查询多个索引的类型
* 
*/
type Props = { a: number; b: string; c: boolean };

type TypeAb = Props['a' | 'b']; // string | number (使用字符串字面量的联合类型,获取属性
                                // a 和 b 对应的类型)。
type Types = Props[keyof Props]; // string | number | boolean (使用keyof操作符获取
                                 // Props中所有键对应的类型)。

16.类型声明文件