TypeScript入门 | 青训营笔记

168 阅读15分钟

这是我参与「第四届青训营 」笔记创作活动的的第3天

前言

1. 什么是TypeScript?

TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。

TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 2015 年的 ECMAScript 和未来的提案中的特性,比如异步功能和 Decorators,以帮助建立健壮的组件。下图显示了 TypeScript 与 ES5、ES2015 和 ES2016 之间的关系:

image-20220803200524945.png

2. TypeScript的特点

TypeScript 主要有 3 大特点:

  • 始于 JavaScript,归于 JavaScript

TypeScript 可以编译出纯净、 简洁的 JavaScript 代码,并且可以运行在任何浏览器上、Node.js 环境中和任何支持 ECMAScript 3(或更高版本)的 JavaScript 引擎中。

  • 强大的类型系统

类型系统允许 JavaScript 开发者在开发 JavaScript 应用程序时使用高效的开发工具和常用操作比如静态检查和代码重构。

  • 先进的 JavaScript

TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 2015 年的 ECMAScript 和未来的提案中的特性,比如异步功能和 Decorators,以帮助建立健壮的组件。

3. TypeScript与JavaScript的对比

TypeScriptJavaScript
JavaScript 的超集用于解决大型项目的代码复杂性一种脚本语言,用于创建动态网页。
可以在编译期间发现并纠正错误作为一种解释型语言,只能在运行时发现错误
强类型,支持静态和动态类型弱类型,没有静态类型选项
最终被编译成 JavaScript 代码,使浏览器可以理解可以直接在浏览器中使用
支持模块、泛型和接口不支持模块,泛型或接口
支持 ES3,ES4,ES5 和 ES6 等不支持编译其他 ES3,ES4,ES5 或 ES6 功能
社区的支持仍在增长,而且还不是很大大量的社区支持以及大量文档和解决问题的支持

image-20220803194339202.png

强类型语言与弱类型语言

  • 强类型语言是一种强制类型定义的语言,即一旦某一个变量被定义类型,如果不经强制转换,那么它永远就是该数据类型。如Java,C++等。
  • 而弱类型语言是一种弱类型定义的语言,某一个变量被定义类型,该变量可以根据环境变化自动进行转换,不需要经过现行强制转换。如JavaScript,PHP等。

静态类型

  • 可读性增强:基于语法解析TSDoc,ide增强
  • 可维护性增强:在编译阶段暴露大部分错误
  • 多人合作的大型项目中,获得更好的稳定性和开发效率

JS的超集

  • 包容于兼容所有JS特性,支持共存
  • 支持渐进式引入与升级(兼容JavaScript代码)

4. TypeScript如何编译为JavaScript

首先我们安装TypeScript,打开命令行窗口

npm install -g typescript 

安装完成后,在控制台输入如下命令,检查是否安装成功(4.x)

tsc -v  #用于查看typescript编译器的版本

示意图

如上图,ts文件通过tsc编译器,生成普通的js文件。接下来,就可以使用node命令执行这个普通的js文件。

步骤如下:

tsc test.ts
node test.js

1. 基础类型

布尔值

let isSure: boolean = false;

数字

let decLiteral: number = 6; //十进制
let hexLiteral: number = 0xf00d; //十六进制
let binaryLiteral: number = 0b1010; //八进制
let octalLiteral: number = 0o744; //二进制

字符串

let firstName: string = '张';
let lastName: string = "三";
let age:number = 18;
let fullName: string = `${firstName}${lastName}今年${age}岁`;

Any

  • 有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any类型来标记这些变量:
let list: any[] = [1, true, "free"];
​
list[1] = 100;

Null 和 Undefined

  • 默认情况下nullundefined是所有类型的子类型。 就是说你可以把 nullundefined赋值给number类型的变量。
let a1: null = null;
let a2: undefined = undefined;

Void

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

function warnUser(): void {
    console.log("HI");
}

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

let unusable: void = undefined;

数组

let arr1: number[] = [10, 20, 30];
let arr2: Array<number> = [10, 20, 304, 50];
let arr3: [string, number, boolean] = ['sds', 12, true]; //每个数据类型一致与定义一致,位置确定

enum枚举

  • enum类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。
enum Color {
        Red,
        Blue,
        Green
}
​

默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 1开始编号:

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

或者全部采用手动赋值

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

枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:

enum Color {
        Red,
        Blue,
        Green
}
console.log(Color.Blue); // 0
console.log(Color[0]); // Red

枚举类型的元素也可以赋值为字符串:

enum EnumExample{
     add = '+',
     mult = '*'
}
EnumExample['add'] === '+'; // true
EnumExample['mult'] === '*'; // true

Object

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

类型断言

有时候你会遇到这样的情况,你会比TypeScript更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你,程序员,已经进行了必须的检查。

类型断言有两种形式。其一时”尖括号<>“语法:

let someValue: any = "this is a string"; //任意类型let strLength: number = (<string>someValue).length; // 使用<string>进去确定someValue为string类型

另一个as语法:

let someValue: any = "this is a string";
​
let strLength: number = (someValue as string).length;

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

联合类型 |

  • 联合类型使我们可以为变量指定多个参数类型
let k1:number|string  = "fd";
k1 = 2;

2. 接口

2.1、概述

  • TypeScript的核心原则之一是对值所具有的结构进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。 在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

2.2、起步

通过简单示例来观察接口是如何工作的:

function printLabel(labelledObj: { label: string }) {
  console.log(labelledObj.label);
}
​
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

此段代码,函数定义的参数为一个对象,当调用printLabel函数时,会对入参进行检查:是否有名为label类型为string的属性。需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。 然而,有些时候TypeScript却并不会这么宽松,我们下面会稍做讲解。

下面重写上面的例子,这次使用接口来描述:必须包含一个label属性且类型为string

interface LabelledValue {
  label: string;
}
​
function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}
​
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

LabelledValue接口就好比一个名字,用来描述上面例子里的要求。 它代表了有一个 label属性且类型为string的对象。 需要注意的是,我们在这里并不能像在其它语言里一样,说传给 printLabel的对象实现了这个接口。我们只会去关注值的外形。 只要传入的对象满足上面提到的必要条件,那么它就是被允许的。

可选属性 ?

  • 接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。使用 ?在属性名字后面进行标记,即为可选属性。
interface SquareConfig {
  color?: string;
  width?: number;
}
​
//function 函数名(函数参数):返回值类型{
//   ...
//}
function createSquare(config: SquareConfig): {color: string; area: number} {
  let newSquare = {color: "white", area: 100};
  if (config.color) {
    newSquare.color = config.color;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}
​
let mySquare = createSquare({color: "black"});

只读属性 readonly

一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly来指定只读属性:

interface Point {
    readonly x: number;
    readonly y: number;
}

可以通过赋值一个对象字面量来构造一个Point。 赋值后, xy再也不能被改变了。

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

TypeScript具有ReadonlyArray<T>类型,它与Array<T>相似,只是它是只读数组,不能修改,也不能使用使数组改变的方法:

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

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

a = ro as number[];

注意: 当约束中某个属性为具体值时,只能取这几个值。

image-20220804011326963.png

2.3、为函数类型定义约束

  • 接口能够描述JavaScript中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。

为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。

interface SearchFunc {
  (source: string, subString: string): boolean;
}

这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。 下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。

let mySearch1: SearchFunc = function(source: string, subString: string) {
  let result = source.search(subString);
  return result > -1;
}
​
//注意:对于函数类型的类型检查,函数的参数名不需要与接口里定义的名字相匹配
let mySearch2: SearchFunc = function(src: string, sub: string): boolean {
  let result = src.search(sub);
  return result > -1;
}
​

函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。 如果你不想指定类型,TypeScript的类型系统会推断出参数类型,因为函数直接赋值给了 SearchFunc类型变量。 函数的返回值类型是通过其返回值推断出来的(此例是 falsetrue)。 如果让这个函数返回数字或字符串,类型检查器会警告我们函数的返回值类型与 SearchFunc接口中的定义不匹配。

let mySearch3: SearchFunc = function(src, sub) {
    let result = src.search(sub);
    return result > -1;
}

2.4、为可索引的类型定义约束

  • 与使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到”的类型,比如a[10]ageMap["daniel"]。 可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。 让我们看一个例子:
interface StringArray {
  [index: number]: string;
}
​
let myArray: StringArray  = ["Bob", "Fred"];
​
//myArray对象的索引类型为number 元素为string类型
let myStr: string = myArray[0];

2.5、为类类型定义约束

  • 与C#或Java里接口的基本作用一样,TypeScript也能够用它来明确的强制一个类去符合某种契约。
interface ClockInterface {
    currentTime: Date;
}
​
class Clock implements ClockInterface {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

你也可以在接口中描述方法,在类里实现它:

interface IPerson {
        readonly id: number; //只读
        name: string;
        age: number;
        sex?: string; //可有可无
​
        k: (str: string) => string;
​
        toString(): void
}
​
class Manager implements IPerson {
​
    id: number;
    name: string;
    age: number;
    sex?: string | undefined;
​
    constructor(id: number, name: string, age: number, sex: string | undefined) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
​
    k: (str: string) => string = (str: string) => {
        return str;
    }
​
    toString(): string {
        return this.id + "-" + this.name + "-" + this.age + "-" + this.sex;
    }
}
​
let k: IPerson = new Manager(2, "小明", 18, "男"); //多态

3. 类

3.1、概述

传统的JavaScript程序使用函数和基于原型的继承来创建可重用的组件,但对于熟悉使用面向对象方式的程序员来讲就有些棘手,因为他们用的是基于类的继承并且对象是由类构建出来的。 从ECMAScript 2015,也就是ECMAScript 6开始,JavaScript程序员将能够使用基于类的面向对象的方式。 使用TypeScript,我们允许开发者现在就使用这些特性,并且编译后的JavaScript可以在所有主流浏览器和平台上运行,而不需要等到下个JavaScript版本

3.2、创建类

class User {
    id: number;
    name: string;
    age: number;
    sex?: string;
​
    constructor(id: number, name: string, age: number, sex: string) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
​
    toString(): string {
        return this.id + "-" + this.name + "-" + this.age + "-" + this.sex;
    }
}
​
let xiaoming: User = new User(1, "小明", 18, "男"); //类的实例化

this

  • 在上述代码中,我们访问类中成员时都使用了this关键字
  • this关键字指代该类,表示我们可以通过this调用类的成员(属性和方法)

3.3、类的继承

  • 在TypeScript里,我们可以使用常用的面向对象模式。 基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类。
class Person{
​
    toString():string{
        return "I am people";
    }
​
}
​
class Student extends Person{
​
    sayHi():string{
        return "Hi,I am Student";
    }
​
}
​
const zhangsan:Student = new Student();
zhangsan.toString();
zhangsan.sayHi();

这个例子实现了最基本的继承:实例化的对象 zhangsan 可以使用父类的 toString() 以及自己的 sayHi()

更复杂的例子:

class Person{
​
    //类成员
    name:string;
    age:number;
    gender:string;
​
    //构造方法
    constructor(name:string,age:number,gender:string ){
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
​
    //实例方法
    toString():string{
        return this.name+"-"+this.age+"-"+this.gender;
    }
    
}
​
class Student extends Person{
​
    constructor(name:string = "xiaoming",age:number = 18,gender:string = "男"){
        super(name,age,gender); // super() 调用父类的构造方法
    }
​
    sayHi():string{
        super.toString(); //子类中,通过super调用父类中的方法
        return "Hi";
    }
​
}
​
const a:Student = new Student();
a.sayHi();
a.toString();

super

  • 在子类中可以通过 super 关键字调用父类的方法(构造方法及非 private 访问修饰符修饰的方法)
  • 也可以通过 super 关键字调用父类中的属性 (非 private 访问修饰符修饰)

注意: 与前一个例子的不同点是,子类包含了一个构造函数,它 必须调用 super(),它会执行基类的构造函数。 而且,在构造函数里访问 this的属性之前,我们 一定要调用 super()。 这个是TypeScript强制执行的一条重要规则。

3.4、方法的重写和重载

方法的重写

class Animal{
    
    eat(): void{
        console.log("吃东西");
    }
​
}
​
class Cat extends Animal{
    
    eat(): void {
        console.log("舌头舔着吃");
    }
}

通过继承可以对父类的方法进行重写,使得重写的方法根据不同的类而具有不同的功能。

方法的重载

class StrUtil {
​
    length(arg: string): number;
​
    length(arg: number): number;
​
    length(arg: string[]): number;
​
    length(arg: unknown): number {
        if (typeof arg === 'string') {
            return arg.length;
        } else if (typeof arg === 'number') {
            return arg.toString.length;
        } else if (Array.isArray(arg)) {
            return arg.length;
        }
        return 0;
    }
}
​
const strUtil:StrUtil = new StrUtil();
strUtil.length("1234");
strUtil.length(1234);
strUtil.length(["1","2","3"]);

TypeScript中类中方法的重载,不同于Java 语言,而是需要定义一系列函数规则,通过最后一个方法实现其函数体。

对比

  • 方法的重写和重载不同:重写是对父类中的方法进行覆盖;
  • 重载是指本类中的方法,允许有多个同名方法,但方法的参数类型或返回值不同。

3.5、访问修饰符

访问修饰符:描述类中成员的可访问性;

public、protected、private

//创建基础动物类
class Animal {
    protected name: string;
    private age: number;
​
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
​
    run(distance: number = 10): string {
        return `${this.name}跑了${distance}公里`;
    }
}
​
//创建狗类
class Dog extends Animal {
​
    constructor(name: string, age: number) {
        super(name, age);
    }
​
    //重写父类中的方法
    run(distance: number = 2) {
        return `${this.name}跑了${distance}公里`;
    }
}
​
//创建Cat类
class Cat extends Animal {
​
    constructor(name: string, age: number) {
        super(name, age);
    }
​
    //重写父类中的方法
    run(distance: number = 1) {
        super.name;
        // super.age; //属性“age”为私有属性,只能在类“Animal”中访问。
        return `${this.name}跑了${distance}公里`;
    }
}
​
//实例化父对象Animal
const ani: Animal = new Animal("动物", 12);
// ani.name; //属性“name”受保护,只能在类“Animal”及其子类中访问。
// ani.age; //属性“age”为私有属性,只能在类“Animal”中访问。
ani.run();
​
//实例化子对象Dog
const dog: Dog = new Dog("大黄狗", 12);
dog.run();

通过以上示例,我们可以知道:

修饰属性

  • public(默认) 公共的 任何位置都可以访问类中的属性
  • private 私有 类中的属性只能在类中访问,如果在类外使用,可以使用方法进行暴露
  • protected 受保护的 外部是无法使用该属性,可以在该类的子类中使用

修饰方法

  • public 默认可在子类和外部访问,也可在子类中重写
  • private 子类中不能访问也不能重写,不能外部访问
  • protected 可在子类中访问可以重写,但不能外部访问

3.6、readony关键字

readonly 修饰符:

对类中属性进行修饰,修饰后,创建对象后只能读取,不能赋值;

只能在类构造方法中被赋值或者在书写属性时直接赋值

class Person{
​
    readonly name: string =  "张三";
​
    constructor(name:string){
        this.name = name;
    }
​
    sayHi(){
        console.log("HI",this.name);
        //this.name = "小海"; //err 无法分配到 "name" ,因为它是只读属性。
    }
}
​
const person:Person = new Person("小明");
//person.name = "大龙"; //err 无法分配到 "name" ,因为它是只读属性。
​
​
//readonly的另一种用法class Animal{
​
    //构造函数中的name属性使用readonly修饰,那么类中就有一个只读类(成员)属性name了
    constructor(readonly name:string = "大黄"){
        this.name = name;
    }
​
    //构造函数中的name属性使用private修饰,那么类中就有一个私有类(成员)属性name了 
    //依次类推 使用public protected 也有对应的属性name和访问权限
    // constructor(private name:string){
    //     this.name = name;
    // }
​
    //不加任何修饰则类中没有该属性
    // constructor(name:string){
    //     // this.name = name; // err 类型“Animal”上不存在属性“name”。
    // }
​
    sayHi(){
        console.log("HI",this.name);
        //err this.name = "大黄"; //err 无法分配到 "name" ,因为它是只读属性。
    }
​
}
const ani: Animal = new Animal("小黑");
// ani.name = "大龙"; //err 无法分配到 "name" ,因为它是只读属性。

注意: 当我们在构造函数对参数进行 readonly、public、protected、private 修饰时,会自动将该参数作为类的属性。(不加修饰时,没有效果)。

3.7、static关键字

static

可修饰类属性和方法(不包括构造函数) 不需要创建对象即可访问类的属性和方法

class Person {
​
    static age:string;
​
    constructor() {
​
    }
​
    static sayHi(): void{
        console.log("Hi");
    }
}
​
console.log(Person.age);
Person.sayHi();

3.8、存取器

存取器:

TypeScript支持通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。

class Person {
​
    constructor(private firstName: string, private lastName: string) {
        this.firstName = firstName;
        this.lastName = lastName;
​
    }
​
    get fullName(): string { // 注意 "get" 访问器不能具有参数。
        return this.firstName + "-" + this.lastName;
    }
​
    set fullName(val:string) { // 注意 "set" 访问器必须正好具有一个参数
        let names:string[] = val.split('-');
        this.firstName = names[0] ;
        this.lastName = names[1];
    }
}
​
const person:Person = new Person("张","三");
​
console.log(person.fullName); // 获取
​
person.fullName = "李-四"; // 修改

注意

  • 首先,存取器要求你将编译器设置为输出ECMAScript 5或更高。 不支持降级到ECMAScript 3。 其次,只带有 get不带有 set的存取器自动被推断为 readonly。 这在从代码生成 .d.ts文件时是有帮助的,因为利用这个属性的用户会看到不允许够改变它的值。
  • 这与 Java 中的 getters/setters 不同:TypeScript中的 get/set 具有严格限制,且在创建对象后,表现为一个属性。

3.9、抽象类

抽象类:

抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法。

abstract class Animal{
​
    constructor(private age:number){
        this.age = age;
    }
​
    //抽象方法
    abstract eat():string;
​
    //实例方法
    sayHi(){
        console.log("Hi");
    }
​
}
​
class Dog extends Animal{
​
    eat(): string {
        return "舔着吃";
    }
​
}
​
const dahuang:Animal = new Dog(18);
dahuang.eat();
dahuang.sayHi();

4. 函数

4.1、概述

函数是JavaScript应用程序的基础。 它帮助你实现抽象层,模拟类,信息隐藏和模块。 在TypeScript里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义 行为的地方。 TypeScript为JavaScript函数添加了额外的功能,让我们可以更容易地使用。

4.2、创建函数

//命名函数
function add(x:string, y:string):string{
    return x + y;
}
​
// console.log(add(10, 20)); // err 类型“number”的参数不能赋给类型“string”的参数。
console.log(add("10","20"));
​
//匿名函数
const add2 = function(x:number, y:number):number{
    return x + y;
}
​
//箭头函数
const add3 = (x:number, y:number):number =>{
    return x + y;
}

函数的完整写法

//函数的完整写法  变量名:参数类型 => 返回值类型 = 函数实体
const add4:(x:number, y:number) => number = function(x:number, y:number):number{
    return x + y;
}
​
const add5:(x:number, y:number) => number = (x:number, y:number):number =>{
    return x + y;
}

函数类型包含两部分:参数类型和返回值类型。 当写出完整函数类型的时候,这两部分都是需要的。 我们以参数列表的形式写出参数类型,为每个参数指定一个名字和类型。 这个名字只是为了增加可读性。 我们也可以这么写:

const add6:(x:number, y:number) => number = (m:number, n:number):number =>{
    return x + y;
}

只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确。

4.3、推断类型

如果你在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript编译器会自动识别出类型:

// myAdd has the full function type
let myAdd = function(x: number, y: number): number { return x + y; };
​
// The parameters `x` and `y` have the type number
let myAdd: (baseValue: number, increment: number) => number =
    function(x, y) { return x + y; };

4.4、可选参数和默认参数

必须参数

TypeScript里的每个函数参数都是必须的。 这不是指不能传递 nullundefined作为参数,而是说编译器检查用户是否为每个参数都传入了值。 编译器还会假设只有这些参数会被传递进函数。 简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。

function buildName(firstName: string, lastName: string) {
    return firstName + " " + lastName;
}
​
// let result1 = buildName("Bob");                  // error, 参数传入不全
// let result2 = buildName("Bob", "Adams", "Sr.");  // error, 参数传入超过
let result3 = buildName("Bob", "Adams");   

可选参数

当在 JavaScript 中,每个参数都是可选的,可传可不传。没传参的时候,它的值就是undefined。 在TypeScript里我们可以在参数名旁使用 ? 实现可选参数的功能。 比如,我们想让 lastName是可选的:

function buildName(firstName: string, lastName?: string) {
    return firstName + " " + lastName;
}
​
let result2 = buildName("诸葛");  

默认参数

在TypeScript里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是undefined时。 它们叫做有默认初始化值的参数。 让我们修改上例,把last name的默认值设置为"Smith"

function buildName(firstName: string, lastName: string = "明") {
    return firstName + " " + lastName;
}
​
​
let result2 = buildName("诸葛");  
​
let result3 = buildName("诸葛", "亮");   

注意

  • 可选参数必须跟在必须参数后面。
  • 默认参数,不必在必须参数后面
  • 可选参数不能赋初始值

image-20220803235908721.png

## 4.5、剩余参数

必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在JavaScript里,你可以使用 arguments来访问所有传入的参数。

在TypeScript里,你可以把所有参数收集到一个变量里:

//...args:string[]  > 剩余的参数,放在了一个数组中
function showMsg(str:string, ...args:[string,number]){
    console.log(args.length);
    
}
​
function showMsg1(str:string, ...args: string[]){
    console.log(args.length);
​
}
​
function showMsg2(str:string, ...args: Array<string>){
    console.log(args.length);
}
​
​
showMsg("1","2",3); 
showMsg1("1","2","3","4","5");
showMsg2("1","2","3","4","5");

4.6、函数的重载

JavaScript本身是个动态语言。 JavaScript里函数根据传入不同的参数而返回不同类型的数据是很常见的。

function add(x: string, y: string): string;
​
function add(x: number, y: number): number;
​
​
function add(x: string | number, y: string | number): string | number {
    if (typeof x === 'string' && typeof y === 'string') {
        return x + y;
    } else if (typeof x === 'number' && typeof y === 'number') {
        return x + y;
    } else {
        return x.toString() + y.toString();
    }
}

5. 泛型

5.1、概述

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

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

5.2、在函数中使用泛型

示例:

function createArray1(value: string, count: number): string[] {
    const arr: string[] = []
    for (let index = 0; index < count; index++) {
        arr.push(value)
    }
    return arr
}
​
function createArray2(value: number, count: number): number[] {
    const arr: number[] = []
    for (let index = 0; index < count; index++) {
        arr.push(value)
    }
    return arr
}
​
function createArray3(value: any, count: number): any[] {
    const arr: any[] = []
    for (let index = 0; index < count; index++) {
        arr.push(value)
    }
    return arr
}
​
const arr1 = createArray3(11, 3)
const arr2 = createArray3('aa', 3)
/**
 * 通过any 来指定类型 则在调用生成数组的每一项时,就无法获得提示,也就是没有类型推断,这不是好的typescript使用方法
 */
arr1[0].toFixed();  //无提示
arr2[0].split(''); //无提示

以上三个函数 createArray1、2、3的功能相同,但参数类型不同和返回值不同;虽然功能一致,但确需要书写三个函数来实现,且对数组中具体元素进行操作时,无语法提示。我们可以通过泛型来优化:

function createArray4<T>(value: T, count: number): T[] {
    const arr: T[] = []
    for (let index = 0; index < count; index++) {
        arr.push(value)
    }
    return arr
}
​
const arr4 = createArray4<number>(11, 3)
const arr5 = createArray4<string>('aa', 3)
​
arr4[0].toFixed(2); // 有提示
arr5[0].split(''); // 有提示
​
​
//多泛型参数:函数中有多个泛型的参数
function getMsg<K, V>(v1: K, v2: V): [K, V] {
    return [v1,v2];
}
​
const ms = getMsg<number,string>(1,"23");
ms[0].toFixed(2);
ms[1].split("");

可以看出泛型的优点:可以实现代码的重用以及编程IDE 可以对数据类型进行识别,帮助我们进行跟高效的编码。

5.3、在接口中使用泛型

泛型接口:

在定义接口时, 为接口中的属性或方法定义泛型类型;在使用接口时, 再指定具体的泛型类型。

示例:定义一个实体类,用来存储用户的相关信息(id,姓名,年龄); 定义一个dao层类,实现对用户的crud操作。

//POJO
class User {
    id?: number;
    name: string;
    age: number;
​
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}
​
interface IBaseDao<T> {
    data: T[];
​
    //添加一个用户
    add(user: T): number
​
    //根据id获取一个用户
    getUserById(id: number): T
}
​
​
class UserDao implements IBaseDao<User>{
​
    data: User[] = [];
​
    add(user: User): number {
        user.id = Date.now();
        this.data.push(user);
        return user.id;
    }
​
    getUserById(id: number): User {
        let user = <User>this.data.find(item => item.id === id);
        return user;
    }
​
}
​
​
const userDao: UserDao = new UserDao();
const userid: number = userDao.add(new User("yanghi", 18));
const userById: User = userDao.getUserById(userid);

由以上代码可以看出,对接口使用泛型,然后类实现接口时,通过泛型,指定所操作对象的类型。这样的好处是:我们可以操作不同的实体对象,达到一套代码,可以复用出对多个对象的crud操作。

5.4、在类中使用泛型

泛型类:

定义类时, 为类中的属性或方法定义泛型类型; 在创建类的实例时, 再指定特定的泛型类型。

示例:定义一个类,定义一个方法,实现当两个参数为数字时返回和,是字符串时返回拼接值。

class GenericNumber<T> {
    zeroValue!: T;
    add!: (x: T, y: T) => T;
}
​
let myGenericNumber = new GenericNumber<number>()
myGenericNumber.zeroValue = 0
myGenericNumber.add = function (x, y) {
    return x + y;
}
​
let myGenericString = new GenericNumber<string>()
myGenericString.zeroValue = 'abc'
myGenericString.add = function (x, y) {
    return x + y;
}
​
console.log(myGenericString.add(myGenericString.zeroValue, 'test'));
console.log(myGenericNumber.add(myGenericNumber.zeroValue, 12));

5.5、泛型约束

泛型约束:如果我们直接对一个泛型参数取 length 属性, 会报错, 因为这个泛型根本就不知道它有这个属性。

// 没有泛型约束
function fn<T>(x: T): void {
    // console.log(x.length)  // error
}
​
interface Lengthwise {
    length: number 
}
​
// 指定泛型约束
function fn2<T extends Lengthwise>(x: T): void {
    console.log(x.length)
}
​
fn2('abc');    //字符串具有length属性
// fn2(123) // error  类型“number”不满足约束“Lengthwise”。 没有length属性

通过 extends 关键字,对泛型进行约束,使其必须具有 length属性。

5.8、泛型工具类型

为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。不过在具体介绍之前,我们得先介绍一些相关的基础知识,方便读者自行学习其它的工具类型。

1.typeof

在 TypeScript 中,typeof 操作符可以用来获取一个变量声明或对象的类型。

interface Person {
  name: string;
  age: number;
}
​
const sem: Person = { name: 'semlinker', age: 30 };
type Sem= typeof sem; // -> Personfunction toArray(x: number): Array<number> {
  return [x];
}
​
type Func = typeof toArray; // -> (x: number) => number[]

2.keyof

keyof 操作符可以用来暴露对象中的所有 (属性)key 值:

interface Person {
    name: string;
    age: number;
}
​
type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join" 
type K3 = keyof { [x: string]: Person };  // string | number

3.in

in 用来遍历枚举类型:

type Keys = "a" | "b" | "c"type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }

4.infer

在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。

type ReturnType<T> = T extends (
  ...args: any[]
) => infer R ? R : any;

以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。

5.Partial

Partial<T> 的作用就是将某个类型里的属性全部变为可选项 ?

定义:

type Partial<T> = {
  [P in keyof T]?: T[P];
};

在以上代码中,首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性值。中间的 ? 号,用于将所有属性变为可选。

示例:

interface Todo {
  title: string;
  description: string;
}
​
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return { ...todo, ...fieldsToUpdate };
}
​
const todo1 = {
  title: "organize desk",
  description: "clear clutter",
};
​
const todo2 = updateTodo(todo1, {
  description: "throw out trash",
});

在上面的 updateTodo 方法中,我们利用 Partial<T> 工具类型,定义 fieldsToUpdate 的类型为 Partial<Todo>,即:

{
   title?: string | undefined;
   description?: string | undefined;
}

5.7、泛型变量的命名规则

对刚接触 TypeScript 泛型的使用者来说,看到 T 和 E,还有 K 和 V 这些泛型变量时,估计会一脸懵逼。其实这些大写字母并没有什么本质的区别,只不过是一个约定好的规范而已。也就是说使用大写字母 A-Z 定义的类型变量都属于泛型,把 T 换成 A,也是一样的。下面我们介绍一下一些常见泛型变量代表的意思:

  • T(Type):表示一个 TypeScript 类型
  • K(Key):表示对象中的键类型
  • V(Value):表示对象中的值类型
  • E(Element):表示元素类型。

6、类型别名和联合/交叉类型

6.1、类型别名

类型别名用来给一个类型起个新名字

type Message = string | string[];
​
let greet = (message: Message) => {
  // ...
};

允许指定字符串/数字必须的固定值

type IDomTag = 'html' | 'body' | 'div' | 'sapn';
type IOddNumber = 1 | 3 | 5 | 9;

6.2、联合/交叉类型

示例:为书籍列表编写类型 类型声明繁琐,存在较多重复

const bookList = [{
    author: 'xiaoming',
    type: 'history',
    range: '2001-2021'
},{
    author: 'zhangsan',
    type: 'story',
    theme: 'love' 
}];
​
interface IHistoryBook{
    author: string,
    type: string,
    range: string
}
​
interface IStoryBook{
    author: string,
    type: string,
    theme: string
}
​
type IBookList = Array<IHistoryBook | IStoryBook>;

改进:我们可以通过联合/较差类型进行改进

  • 联合类型:IA | IB 联合类型标识一个值可以是几种类型之一(或)
  • 交叉类型:IA & IB 多种类型叠加到一起称为一种类型,它包含了所需的所有类型的特性
type IBookList = Array<{
    author: string;
} & ({
    type: 'history';
    range: string;
}) | ({
    type: 'story';
    theme: string;
})>