TS基础知识大赏~

86 阅读24分钟

TS简介

为什么需要TS

我们知道js是一个非常灵活的语言,他没有类型约束,存在隐式转换,这种灵活有时候是一把双刃剑,带来了便利的同时也会造成很多问题。当系统足够复杂的时候维护难度较大,可能会造成很多运行时错误。

什么是TS

根据官网的描述:

  • ts是由微软进行开发和维护的一种开源的编程语言。

  • ts是JavaScript的超集, 在js的基础上进行了扩展,具有类型系统,可以提供编译时的静态类型检查,完全兼容js,可以编译成js。

TS的特点

  • ts的最终执行是编译成js再执行的, 所以ts是编译型语言。

  • ts在编译阶段会进行类型检查。如果出现错误 会抛出编译时错误 所以ts是静态语言。

  • ts完全兼容js, 而js是弱类型 所以本质上ts还是弱类型的。

  • ts具有更多js没有的特性。

学习TS

tsconfig.json

  • tsconfig.json文件通过compilerOptions字段配置ts的编译行为

  • tsconfig.json所在目录就是这个项目的根目录

  • 可以通过tsc --init命令生成

image.png

数据类型

ts的数据类型有: booleannumberstring,nullundefinedvoidanyarraytupleenumobjectsymbol

这里先介绍前七个类型

boolean,number,string

与js一样, ts中也存在booleannumberstring三种类型

const a: boolean = true
const b: number = 1
const c: string = 'test'

null和undefined

与js一样, ts也存在nullundefined类型

const a: null = null
const b: undefined = undefined
  • tsconfig.jsonstrictNullChecks配置为false的时候,nullundefined是所有类型的子类型,也就是说可以将nullundefined赋值给任何类型包括void, 反之nullundefined只能赋值给他们自己类型和void类型。 image.png
const c: number = null
const d: string = undefined
const e: void = undefined

void

  • 在ts中void是一种数据类型,声明一个void类型的变量没有太大的作用,因为这个变量只能在tsconfig.jsonstrictNullChecks配置为false的时候被赋值为null或者undefined

  • 这个类型一般用来声明函数, 表示这个函数没有返回值。

const func: () => void = () => {}
  • 在js中也存在void, 但是void不是数据类型而是一个操作符关键字, 他表示执行语句并返回 undefined

image.png

  • 通常会被用于防止undefined被篡改, 因为在js中undefined不是一个关键字, 所以会出现以下的情况

image.png

any

在ts中any类型兼容任何其他类型,也就是说其他任何类型都可以赋值给any类型。

ts的类型兼容性简单的说就是是指一个类型是否可以赋值给其它类型。

  • 如果将一个变量声明为any类型意味着编译阶段将不会再对这个变量进行类型检查,且这个变量可以被赋值为任何类型的数据调用任何方法。

image.png 可以看到上面的a可以被复制为boolean也可以被复制为string还可以调用array的方法,但是编译没有报错。

  • 如果声明了一个变量但是没有赋值,那么这个变量将会被推断为any类型。

image.png

变量声明

普通变量

在ts中声明一个变量很简单, 你可以显式的声明变量的类型通过变量名:变量类型=值的形式或者可以不声明变量类型(没有显式声明的变量类型会被推断出来)。

const a: number = 1 // 显式声明
const b = 'test' // 没有声明 类型会被推断出来

函数声明

函数声明主要在参数类型和返回值类型,使用(形参:参数类型) => 返回值类型的形式来进行声明。

let func: (name: string, age: number) => boolean // 参数为一个string类型一个number类型 返回一个布尔值

联合类型

假如说我们定义一个变量,这个变量在某种情况下是string,在另一种情况又是number,这样该怎么声明?这里就需要用到联合类型,表示是几种类型中的其中一个类型,我们使用类型1 | 类型2 |类型n的形式来声明联合类型:

let p: string | number;
p = 'p';
p = 10;

可以看到声明的p表示可以是string或者number,所以后面再给p赋值的时候就可以是string或者number

注意:如果一个类型被声明为联合类型,那么只能访问几种类型的公共部分:

let p: string | number;
p.toString();
// error Property 'length' does not exist on type 'string | number'.
console.log(p.length);

可以看到pstring或者number,但是只有stringlength属性,所以在访问length的时候会出现错误,但是两者都有toString,是两个类型的公共部分,所以调用不会出现错误。

交叉类型

假如说我们现在有一个Person类型,还有一个Student类型,然后现在有一个变量,具有PersonStudent两者的全部属性,那么我们应该怎么声明?需要再创建一个接口将PersonStudent全部再复制一遍么?在这里我们只需要使用交叉类型即可,交叉类型会将几个类型合并成一个类型, 我们使用类型1 & 类型2 & 类型n的形式来定义交叉类型:

interface IPerson {
    name: string;
}

interface IStudent {
    age: number
}

const p: IPerson & IStudent = {
    name: 'p',
    age: 10
}

可以看到pIPersonIStudent的交叉类型,所以具有两者的全部属性。

类型别名

类型别名会给类型起一个新名字,同时也可以像接口一样被用来约束对象的具体结构,类型别名通过type关键字来定义:

type Student = {
    name : string;
    age: number;
}

const a: Student = {
    name: 'a',
    age: 10
}

既然接口和类型别名都可以用来约束对象的具体结构, 那么他们的区别是什么?

两者之间最大的区别就在于接口是可以被extends(继承)和implements(实现)的,但是类型别名是不可以的。

类型别名如果需要在自身基础上进行拓展,可以使用&

type TPerson = { name: string } & {age: number}

const p: TPerson = {
    name: 'p',
    age: 10
}

接口

基本用法

对于简单类型我们可以直接使用基本类型进行声明,那么我们如何定义对象内部的具体结构呢?接口就可以实现这个效果。接口使用关键字interface进行声明:

interface IPerson{
    name: string;
    age: number;
    study(): void // 函数类型 也可以这样定义:study: () => void
}
// 这样就定义了一个名为IPerson的接口,
//它规定这个接口类型的变量都必须包含一个string类型的name属性和一个number类型的age属性,以及一个没有返回值没有参数的函数类型的study属性
const p1: IPerson = {name: 'xiaoming', age: 10, study: () => {}}  // 使用这个接口来声明变量

这是一个最基本简单的接口定义和使用,在这个接口里面所有的属性名和属性类型都是确定的,这个接口声明的的变量都必须准确的匹配接口的属性名和属性类型,不能多也不能少

接口也可以用来描述函数的结构:

interface IWork {
    (isWorking: boolean): number // 这是一个函数结构的定义
}
const work: IWork = (woking) => woking ? 1 : 2

// 注意与对象的函数类型的属性进行区别哦
interface IPerson{
    study(): void // 这样的是对象的一个函数类型的study属性
}

可选属性

如果说希望有一种接口,它包含一个属性,这个属性在某种情况下才存在,也就是可选属性,我们可以使用?来将这个属性标记为可选的:

interface IPerson {
    name: string;
    age: number;
    hobby?: string; // ?标记这个属性是可选的
}

// 声明变量的时候可以包含hobby属性也可以不包含
const p1: IPerson = {name: 'p1', age: 10, hobby: '篮球'}
const p2: IPerson = {name: 'p2', age: 10}

只读属性

一些属性期望在创建变量的时候就进行赋值,后续只能访问读取不允许再进行更改,这样的属性一般会将它标记为只读属性,使用readonly关键字进行修饰:

interface IPerson {
    name: string;
    age: number;
    readonly hobby: string; // readonly标记这个属性是只读的
}

const p1: IPerson = {
    name: 'p1',
    age: 10,
    hobby: 'hobby'
}

p1.age = 20;
p1.hobby = 'hobby2' // error Cannot assign to 'hobby' because it is a read-only property

对于只读的变量的定义可以使用const执行定义表示变量不可再次赋值。

任意属性

考虑一种情况,一个接口在某种情况下可能除了已知的确定属性之外还有其他额外的位置属性,属性类型确定但是属性名不确定,那么我们就可以像下面这样定义:

interface IPerson {
    name: string;
    sex: string;
    [propName: string]: string; // 表示除了name和age之外还可以有任意个数属性名为string类型,值为string类型的属性
}
const p1: IPerson = {
    name: 'p1',
    sex: 'man',
    // 任意个数属性名为string类型,值为string类型的属性
    prop1: 'prop1',
    prop2: 'prop2',
    prop3: 'prop3',
}
  • 任意类型属性名的类型只能是number或者string。
  • 确定属性值的类型要是任意属性值的自子类型。
interface IPerson {
    name: number; // error Property 'name' of type 'number' is not assignable to 'string' index type 'string'.
    [propName: string]: string;
}

可以看到出现了错误, 因number不是string的子类型,如果将name的类型改为any就可以通过, 因为any是任何类型的子类型。

js在es6中加入了class的概念也就是类, ts包含es6种类的所有特性比如:

  • 通过关键字class定义类

  • 具有存取器:存值函数setter和取值函数getter

  • 具有静态方法和静态属性,通过static关键字进行定义

  • 可以通过extends关键字实现类的继承

但是ts还包含一些js没有的特性:

权限:

js中的权限

  • 所谓权限就是指这个方法或者属性可以被访问的范围,public属性可以被任意访问,private属性(方法)只能在类的内部被访问,protected属性或方法只能在这个类内部或者子类中被访问。

  • 在es6中没有一个直接的方法来实现,通常是通过直接将私有方法移出类的内部。

  • 在es2022中正式提供了一个方法:方法(属性)名之前使用#来表示这是一个private方法(属性)。

// hello.js
const _privateFunc = (args) => console.log(args)
class Hello{
    classFunc(args) {
        _privateFunc.call(this, args)
    }
}
module.exports = {
    Hello
}
// h.js
const { Hello } = require('./hello.js')
const h1 = new Hello();
h1.classFunc(1)

这样只向外暴露Hello这个类,然后在类的内部调用_privateFunc这个方法(一般私有方法名私有属性名以下划线开头),外部的任何地方都没有办法调用_privateFunc,就相当于实现了私有性, 再看下面ES2022的例子:

class Hello {
    #name: string;
    #privateFunc(){
        return 'es2022 方法(属性)名之前使用`#`来表示这是一个私有方法(属性)'
    }
}

const h1 = new Hello();
h1.name = 'test' // error
h1.privateFunc(1) // error

ES2022可以直接使用#来声明私有方法和属性

ts中的权限

private和protected

  • 在ts中,除了private还多了一个受保护的protected, 默认情况所有的属性和方法都是public,ts其通过privateprotected关键字来修饰属性和方法实现权限。

  • public属性可以被任意访问,private属性(方法)只能在类的内部被访问,protected属性(方法)只能在这个类内部或者子类中被访问。

class Person {
    name: string;
    private age: number;
    protected sex: string;
    private work(sex: string) {
        this.sex = sex; // protected属性可以在类的内部被访问
    }
    protected study(age: number) {
        this.age = age; // private属性可以在类的内部被访问
    }
}

class Student extends Person {
    play(age: number, sex: string){
        // protected属性可以在子类的内部被访问
        this.sex = sex;
        this.study(10);
        // private属性只能在类的内部被访问 子类内部也无法访问
        this.age = age;
        this.work('man')
    }
}

const p = new Person();
// public 的属性方法在哪里都可以被访问
p.name = 'name' 

// 非public的属性和方法在外部无法被访问
p.age = 20
p.sex = 'man';
p.work('man')
p.study(10)

只读属性

只读属性只能在类的内部声明的时候赋值或者在类实例化的时候赋值,其他情况都不能再赋值,使用readonly关键字来修饰只读属性:

class  Person{
    readonly name: string;
    readonly age = 10; // 声明属性的时候进行赋值
    constructor(name: string) {
        this.name = name;
    }
    work(name: string) {
        this.name = name // error 只能在实例化的时候或者声明属性的时候赋值
    }
}
const p = new Person('name');
p.name = 'name' // error 只能在实例化的时候或者声明属性的时候赋值

抽象类

抽象类有两个明确的特点:

  • 不能直接实例化,只能通过子类来实例化。

  • 抽象类的子类必须实现父类中所有的抽象方法(抽象方法: 只有方法签名没有具体实现的方法体)。

在js中是没有办法直接定义一个抽象类的,只能通过new.target来判断当前通过new来调用的构造函数是不是当前这个抽象类来实现。

在ts中则可以直接通过abstract关键字来直接定义抽象类:

abstract class Person{
    name: string;
    abstract study(): void // 必须在子类中实现
}

class Student extends Person {
    study(): void{} // 实现父类的抽象方法
}

const p = new Person() // error Cannot create an instance of an abstract class.

const s = new Student();

类和接口

约束对象结构

我们知道接口可以用来约束对象的结构,同样的类也可以用作约束变量的内部结构,看下面的例子:

class Person{
    name: string;
    age: number;
}

const p: Person = {
    name: 'name',
    age: 10,
    sex: 'man' // error Type '{ name: string; age: number; sex: string; }' is not assignable to type 'Person
}

可以看到当一个变量被声明为某个类的类型之后就必须符合这个类的结构,也可以用来限制变量的结构

既然类和接口都可以用来约束变量的具体结构,那么他们之间有什么区别呢?

两者最主要的区别就在于:接口只在编译阶段起作用,编译完成输出的js文件中不会保留接口的定义,但是类再编译之后还是会输出js代码,看下面的例子:

image.png

可以看到在编译之后interface并没有任何输出,而class会完全输出在js中,所以如果大量的使用class去约束变量结构 很可能会造成编译后的js文件体积过大。

另外class还可以提供变量方法的具体实现方式,功能也比interface更多,不仅仅用于约束变量结构。

类实现接口

现在我们知道了类和接口都可以约束变量的结构, 那么怎么才可以约束类的结构呢?

我们就可以利用接口,让类使用关键字implements来实现接口从而约束类的结构,实现接口的类必须包含接口中所有的属性,实现所有方法看下面的例子:

interface IPerson{
    name: string;
    age: number;
    work: () => void
}

// error  Class 'Person' incorrectly implements interface 'IPerson'.
class Person implements IPerson{
    age: number;
    // name: string;
    sex: string;

    work(): void {
    }
    study(): string {
        return 'study'
    }
}

可以看到实现接口的类必须包含接口中所有的属性,实现所有的方法, 因为没有name属性,所以出现了报错,但是可以在接口的基础上再扩展自己的属性和方法。

接口继承类

  • 为了更方便的扩展和分割公共的接口部分,接口可以继承类。

  • 对于没有privateprotected属性的类,接口会继承这个类的所有非static的属性和方法但不包含方法实现,只有方法签名,

  • 其次如果被继承的类有非公共属性或方法,那么继承这个类的接口就只能被他的子类实现,看下面的例子:

class Person {
    name: string;
    static sex: string;
    work(){}
}
interface IPerson extends Person{
    age: number;
}

const p: IPerson = {
    name: 'p',
    age: 10,
    sex: 'sex', // error '{ name: string; age: number; sex: string; work(): void; }' is not assignable to type 'IPerson'.
    work() {
        console.log('work');
    }
}

可以看到接口没有继承类的静态属性sex, 所以在尝试给p变量定义sex属性的时候出现了错误

对于接口继承有privateprotect属性的类:

class Person {
    name: string;
    private age: number;
    protected sex: string;
    private work(): void{};
    protected study(): void{};
}
interface IPerson extends Person{
    hobby: string;
    play(): void;
}

class Teacher implements IPerson{
    // error Class 'Teacher' incorrectly implements interface 'IPerson'.
}

//error  Property 'age' is private in type 'IPerson' but not in type '{ name: string; age: number; sex: string; hobby: string; work(): void; study(): void; }'.
const p: IPerson = {
    name: 'name',
    age: 10,
    sex: 'sex',
    hobby: 'hobby',
    work(){},
    study(){}
}

class Student extends Person implements IPerson {
    hobby: string;
    play(){};
}

const s = new Student();

可以看到继承有privateprotect属性和方法的类的接口直接实现和声明变量都是不行的,只有通过类的子来实现它。

不过一般对于具有private和protected属性和方法的类不推荐被接口继承。

接口继承接口

接口继承接口比较简单,同样是为了更好的复用接口的公共部分,看下面的例子:

interface IPerson {
    name: string;
}

interface ITeacher {
    age: number;
}

// 继承单个接口
interface IStudent extends IPerson {
    hobby: string;
}
const s: IStudent = {
    name: 'student',
    hobby: 'hobby'
}

// 继承多个接口
interface IMan extends IPerson, ITeacher, IStudent {}
const m: IMan = {
    name: 'student',
    hobby: 'hobby',
    age: 30
}

可以看到接口继承接口之后就会合并两个接口的定义,同时一个接口可以继承多个接口, 同样的接口也可以继承多个类, 一个类也可以实现多个接口。

额外类型检查

我们说普通的接口和类在声明变量的时候都需要准确的匹配属性名和属性类型不能多也不能少, 但是有一种迂回的方法可以添加额外的属性:

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

class Student {
    name: string;
    age: number;
}

let p: IPerson = {
    name: 'name', 
    age: 10, 
    // error Object literal may only specify known properties, and 'sex' does not exist in type 'IPerson'.
    sex: 'man'
}; 
const p2 = {name: 'name', age: 10, sex: 'man'};
p = p2;

let s: Student = {
    name: 'name', 
    age: 10, 
    // error Object literal may only specify known properties, and 'sex' does not exist in type 'Student'
    sex: 'man'
};
const s2 = {
    name: 'name', 
    age: 10, 
    sex: 'man'
}
s = s2;

可以看到当使用对象字面量的时候直接给ps赋值因为多了sex属性会出现错误,但是将p2s2赋值给ps的时候并没有出现错误,这是因为ts针对对象字面量会进行额外属性检查,检查对象是否含有interfaceclass定义之外的属性,但是对于变量却不会

泛型

假如说现在有一个复印的函数,你输入什么他就会返回一个同样类型给你,这个函数应该怎么定义?当然我们可以使用any进行声明像下面这样:

interface IWork {
    (source: any): any
}

这样定义当然可以实现我们想要的功能,但是无法从这个定义中get到输入和输出是同一个类型这个信息,所以像这种情况更好的方式就是使用泛型。

什么是泛型

泛型就是类型参数化,简单的说就是将数据类型当作一个参数来使用,一般使用<>来定义一个泛型。泛型有很多应用。

泛型函数

我们可以在函数上使用泛型,看下面的例子:

const myPrint: <T>(arg: T) => T = (arg) => arg
myPrint<number>(1) // 一般<number>可以省略掉
myPrint('a')
myPrint({name: 'test'})

可以看到在函数上<T>声明了一个类型的参数, 在调用的时候将类型作为一个参数传入,像第一个调用方式一样,但是通常我们会省去<number>ts会根据我们传入的参数来确定T的类型。

泛型接口

在接口中我们也可以使用泛型, 看下面的例子:

interface IPerson<T>{
    name: T;
    age: number;
    work(arg: T): T;
}

const p1: IPerson<string> = {
    name: 'p1',
    age: 10,
    work(arg: string): string {
        return 'work';
    }
}

在定义接口的时候使用类型参数,这样就定义了一个泛型接口,在接口内部就可以使用这个类型参数,同时在使用这个接口声明变量的时候就需要传入类型参数。

泛型类

类也可以使用泛型,看下面的例子:

class Person<T> {
    name: T;
    constructor(name: T) {
        this.name = name
    }
}
const p = new Person<string>('p')

像这样就定义了一个动态类型的类,在类的内部可以使用这个类型参数, 在创建这个类的实例的时候就需要传入类型参数。

泛型约束

考虑一种情况,假设一个泛型函数,在这个函数内我们需要访问name属性,那就需要保证我们传入的类型参数上必须包含name属性,看下面的例子:

const func = <T>(arg: T) => arg.name // error Property 'name' does not exist on type 'T'

可以看到, 如果不做任何其他处理直接使用泛型会出现错误,因为不能保证调用的时候传入的所有类型都包含name属性,比如我传入string类型就不包含name属性,那么我们就需要给这个泛型加上一些限制,也就是泛型约束。

泛型约束使用extends关键字来实现, 看下面的例子:

interface IPerson {
    name: string
}
const func = <T extends IPerson>(arg: T) => arg.name
const param = {name: 'test', age: 10}
func(param)
func(1) // error Argument of type 'number' is not assignable to parameter of type 'IPerson'.

这里使用extends让T继承IPerson接口,那么T中就必然包含name属性了,在调用的时候也就会检查传入的参数是否包含name属性。

类型推断

简单的说就是当你没有明确的指出变量的类型的时候,ts会按照一定的规则自己推断出变量的类型, 就像下面的例子:

let a = 1
a = 'a' // error => Type 'string' is not assignable to type 'number'

这里在声明a变量的是并没有明确指出anumber类型。但是进行了赋值为1,那么ts就会推断这个a变量是number类型,所以在下面将字符串赋值给a的时候就会出现错误。

这种推断是从右向左在变量初始化的时候进行的,如果这个变量在声明的时候没有赋值,那么这个变量就会被推断成any类型,不再进行类型检查。

这是一种最简单情况, 类型推断还会发生在很多其他的情况下 根据上下文来推断的, 看下面的例子

const a = {
    func: (args: {id:number} )  => {}
}
a.func = (param) => console.log(param.name); // error => Property 'name' does not exist on type '{ id: number }'

这里在定义a的时候就已经将a.func的类型推断出来了, 所以在下面对a.func进行赋值的时候会按照a.func的类型来推断检查赋值函数的类型,因此param的类型就是{id:number}, 所以去param上读取name属性就会报错。

类型兼容性

类型系统:结构子类型和名义类型

在ts中采用的是结构子类型,所谓结构子类型就是指所有的类型检查都是基于结构的而不需要明确的声明,反之名义类型则会对明确的声明或类型名称进行检查:

class Person {
    name: string;
    age: number;
}

class Student {
    name: string;
    age: number;
}

const p: Person = new Student();

可以看到p被声明为Person类型但是却可以赋值为一个Student类型,因为StudentPerson在结构上是相似的(兼容的)。这就是结构子类型,不会检查具体的类型声明和名称,只要结构兼容就可以,但是这种写法在java等名义类型的语言中是不可行的。

类型兼容

那么什么是类型兼容呢?简单理解就是如果A兼容B, 那么在应该使用A的地方也可以使用B。

就像上面的例子,本身P应该赋值为一个Person类型的值, 但是Person兼容Student, 所以也可以赋值为Student

那么如何判断A是否兼容B呢?

普通类型:

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

interface IStudent {
    name: string;
    age: number;
    sex: string;
}

let p: IPerson = {
    name: 'p',
    age: 10
}

let s: IStudent = {
    name: 's',
    age: 20,
    sex: 'sex'
}

p = s;
s = p; // error Property 'sex' is missing in type 'IPerson' but required in type 'IStudent'.

可以看到IStudent可以赋值给IPerson因为这种情况下的检查是从左向右的,会检查左边类型的所有属性在右边的类是中是否可以找到。 如果可以找到那么右边类型就是兼容左边类型的。也就是IPerson兼容ISTudent, 所以IPerson不能赋值给IStudent, 因为IStudentsex属性在IPerson中没有找到, 所以IStudent不兼容IPerson。

函数类型:

函数类型主要需要判断函数的参数和返回值了

参数:

interface IWork {
    (name: string): void
}

interface IStudy {
    (name: string, age: number): void
}

let w: IWork = (name: string) => {}
let s: IStudy = (name: string, age: number) => {}

s = w;
// Type 'IStudy' is not assignable to type 'IWork'.
w = s;

可以看到IWork可以赋值给IStudy, 因为对于函数参数是从右向左检查的,右边函数的参数需要在左边函数中都找到声明,左边函数就是兼容右边函数的。所以IStudy不能赋值给IWork, 因为IStudent中的age参数在IWork中没有找到声明。

返回值

interface IWork {
    (): {name: string}
}

interface IStudy {
    (): {name: string, age: number}
}

let w: IWork = () => ({name: 'name'})
let s: IStudy = () => ({name: 'name', age: 20})

w = s;
// Property 'age' is missing in type '{ name: string; }' but required in type '{ name: string; age: number; }'
s = w;

可以看到IStudy可以赋值给IWork, 在参数一样的时候检查函数的返回值,当左边函数的返回值兼容右边函数的返回值的时候,左边函数就兼容右边函数。 所以IWork不能赋值给IStudy, 因为{name: string, age: number}不兼容{name: string}类型。

类型断言

类型断言简单的说就是让ts按照你的方式来进行检查。

类型断言一共有两种形式:尖括号法和as语法。

interface IPerson {
    name: string;
}

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

const func = (param: IPerson) => {、
    // param是IPerson类型所以无法访问sge属性
    console.log(param.age);

    // 将param断言为IStudent 就可以访问age属性了
    console.log((param as IStudent).age); // as语法
    console.log((<IStudent>param).age); // 尖括号语法
}

可以看到因为param被声明为IPerson类型,ts会按照IPerson检查param发现param。里面只有name属性,所以在func内无法访问paramage属性,但是我们如果我们将param断言成IStudent,那么ts就会按照IStudent检查param,发现里面是name属性的,就可以访问name属性。

但是也不是任何类型之间都可以随意断言。

// error Conversion of type 'number' to type 'string' may be a mistake
const func = (param: number) => console.log((param as string).length);

可以看到number就不能被断言成string

断言的条件

简单的说如果两种类型之间存在一定的兼容关系就可以互相断言,也就是如果A兼容B, 那么A可以被断言为B,B也可以被断言为A。

所以上面第一个例子之所以IPersonIStudent能互相断言就是因为IPerson是兼容IStudent的,如果两者不兼容那么也就不能断言了:

interface IPerson {
    name: string;
    sex: string;
}

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

const func = (param: IPerson) => {
    // error Conversion of type 'IPerson' to type 'IStudent' may be a mistake
    console.log((param as IStudent).age); 
    // error Conversion of type 'IPerson' to type 'IStudent' may be a mistake
    console.log((<IStudent>param).age); 
}

这样IPersonIStudent就不存在兼容关系,所以也就不能进行断言。

一些断言的特殊应用:

  • any类型兼容任何类型, 所以任何类型都能断言成any类型,any类型也能断言成任何类型

  • 如果想访问联合类型中非公共部分的属性方法,可以使用断言将变量断言成联合属性中的一个属性

注意断言只能在ts编译时绕过错误,有可能造成运行时错误。

interface IPerson {
    name: string;
    work(): string;
}

interface IStudent {
    name: string;
    study(): number;
}

const func = (param: IPerson | IStudent) => {
    (param as IStudent).study()
}
const p: IPerson = {
    name: 'p',
    work: () => 'work'
}

func(p)

可以看到在func中我们将param声明为IPersonIStudent的联合类型,为了调用IStudentstudy,我们将param断言成IStudent,然后调用func的时候传入了一个IPerson的变量,这样在编译时不会出现错误,但是在运行时因为p没有study方法就会出现运行时错误。

最后

这是我的第一篇文章,先试试水,后面会把没讲完的内容继续做完,如果有不对的地方希望大家多多指教~