typescript 学习笔记2

122 阅读9分钟

上篇介绍了typescript的一些基本定义和数据类型,这里介绍他们的进阶定义和用法

1. 结构类型系统

结构类型是一种只使用其成员来描述类型的方式。 它正好与名义(nominal)类型形成对比。 TypeScript的结构性子类型是根据JavaScript代码的典型写法来设计的。 因为JavaScript里广泛地使用匿名对象,例如函数表达式和对象字面量,所以使用结构类型系统来描述这些类型比使用名义类型系统更好。 ** TypeScript结构化类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的属性。** 其一切兼容性目标也都是为了保证程序的安全

1.1 基本类型的兼容性

//基本数据类型也有兼容性判断
let num : string|number;
let str:string='nongmin';
num = str;

1.2 接口的兼容性

如果传入的变量和声明的类型不匹配,TS就会进行兼容性检查 原理是Duck-Check,就是说只要目标类型中声明的属性变量在源类型中都存在就是兼容的

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

interface Person {
    name: string;
    age: number;
    gender: number
}
// 要判断目标类型`Person`是否能够兼容输入的源类型`Animal`
function getName(animal: Animal): string {
    return animal.name;
}

let p = {
    name: 'nongmin',
    age: 99,
    gender: 0
}
//只有在传参的时候两个变量之间才会进行兼容性的比较,赋值的时候并不会比较,会直接报错
getName(p);          // Ok
//只有在传参的时候两个变量之间才会进行兼容性的比较,赋值的时候并不会比较,会直接报错
let a: Animal = {     // Error
    name: 'zhanshen',
    age: 10,
    gender: 0
}

1.3 类的兼容性

TypeScript是结构类型系统,只会对比结构而不在意类型

class Animal{ name!: string }
class Bird extends Animal{ swing!: number }
let a: Animal;
a = new Bird();
let b: Bird;
// 并不是父类兼容子类,而是子类不兼容父类
b = new Animal();  // Error,找不到swing属性

//没有关系的两个类的实例也是可以兼容的
class Animal1{ name: string }
class Bird1{ name: string }
let a: Animal1 ;
a = new Bird();
let b: Bird;
b = new Animal();

1.4 函数的兼容性

比较函数的时候是要先比较函数的参数,再比较函数的返回值

1.4.1 比较参数

入参可以少但是不能多

let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // OK
x = y; // Error

1.4.2 比较返回值

类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。 也可以理解为,返回值数据结构可以多余目标的返回值数据结构

type GetPerson = ()=>{name:string,age:number};
let getPerson:GetPerson;
//返回值一样可以
function g1(){
    return {name: 'zhanshen',age: 10};
}
getPerson = g1;  // OK
//返回值多一个属性也可以
function g2(){
    return {name: 'zhanshen',age: 10,gender: 'male'};
}
getPerson = g2;  // OK
//返回值少一个属性可不行
function g3(){
    return {name: 'zhanshen'};
}
getPerson = g3;  // Error
//因为有可能要调用返回值上的方法 getPerson().age.toFixed();

1.4.3 函数的协变与逆变

协变(Covariant):只在同一个方向; 逆变(Contravariant):只在相反的方向; 双向协变(Bivariant):包括同一个方向和不同方向; 不变(Invariant):如果类型不完全相同,则它们是不兼容的。 上面介绍的函数参数和返回值的规律可以得出: 返回值类型是协变的,而参数类型是逆变的 返回值类型可以传子类,参数可以传父类 参数逆变父类 返回值协变子类

但是在 TypeScript 中, 参数类型是双向协变的 ,也就是说既是协变又是逆变的,而这并不安全。但是现可以在 TypeScript 2.6 及以后的版本中通过 --strictFunctionTypes 或 --strict 标记来修复这个问题

1.5 泛型的兼容性

泛型在判断兼容性的时候会先判断具体的类型,然后再进行兼容性判断

    //1.接口内容为空没用到泛型的时候是可以的
    interface Empty<T>{}
    let x!:Empty<string>;
    let y!:Empty<number>;
    x = y;  // OK

    //2.接口内容不为空的时候不可以
    interface NotEmpty<T>{ data:T }
    let x1!:NotEmpty<string>;
    let y1!:NotEmpty<number>;
    x1 = y1; // Error
    //实现原理如下,先判断具体的类型再判断兼容性
}

1.6 枚举的兼容性

枚举类型与数字类型兼容,并且数字类型与枚举类型兼容 不同枚举类型之间是不兼容的

2. 类型保护

类型保护是一些表达式,他们在编译的时候就能通过类型信息确保某个作用域内变量的类型 类型保护就是能够通过关键字判断出分支中的类型

2.1 可辨识的联合类型

联合类型(Union Types):表示取值可以为多种类型中的一种,未赋值时联合类型上只能访问两个类型共有的属性和方法。

// 利用联合类型中的共有字段进行类型保护的一种技巧
// 相同字段的不同取值就是可辨识
interface WarningButton{
  class:'warning',
  text1:'修改'
}
interface DangerButton{
  class:'danger',
  text2:'删除'
}
type Button = WarningButton|DangerButton;
function getButton(button:Button){
 if(button.class=='warning'){
  console.log(button.text1);
 }
 if(button.class=='danger'){
  console.log(button.text2);
 }
}

2.2 typeof 类型保护

function double(input: string | number | boolean) {
    if (typeof input === 'string') {
        return input + input;
    } else {
        if (typeof input === 'number') {
            return input * 2;
        } else {
            return !input;
        }
    }
}

2.3 instanceof类型保护

    class Animal { name!: string;}
    class Bird extends Animal { swing!: number}
    function getName(animal: Animal) {
        if (animal instanceof Bird) {
            console.log(animal.swing);
        } else {
            console.log(animal.name);
        }
    }

2.4 null类型保护

如果开启了strictNullChecks选项,那么对于可能为null的变量不能调用它上面的方法和属性

let s = "foo";
s = null; // 错误, 'null'不能赋值给'string'
let sn: string | null = "bar";
sn = null; // 可以
sn = undefined; // error, 'undefined'不能赋值给'string | null'

2.5 链判断运算符

链判断运算符是一种先检查属性是否存在,再尝试访问该属性的运算符,其符号为 ?. 如果运算符左侧的操作数 ?. 计算为 undefined 或 null,则表达式求值为 undefined 。否则,正常触发目标属性访问,方法或函数调用。

2.6 in操作符

in 运算符可以被用于参数类型的判断

    interface Bird {swing: number;}
    interface Dog { leg: number;}
    function getNumber(x: Bird | Dog) {
        if ("swing" in x) {
          return x.swing;
        }
        return x.leg;
    }

2.7 自定义的类型保护

类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个类型谓词:

interface Bird { swing: number; }
interface Dog { leg: number; }

//没有相同字段可以定义一个类型保护函数
function isBird(x: Bird | Dog): x is Bird{
 return (x).swing == 2;
 //return (x as Bird).swing == 2;
}

function getAnimal(x: Bird | Dog) {
 if (isBird(x)) {
   return x.swing;
 }
 return x.leg;
}

3. 类型变换

3.1 类型推断

TypeScript 能根据一些简单的规则推断变量的类型

  • 从右向左: 变量的类型可以由定义推断
let foo = 1; // foo 是 'number'
let bar = 'zhanshen'; // bar 是 'string'
//foo = bar; // Error: 不能将 'string' 赋值给 `number`
  • 返回类型能被 return 语句推断
function add(a: number, b: number) {
    return a + b;
}
let c = add(1,2);
  • 函数参数类型/返回值类型也能通过赋值来推断
type Sum = (a: number, b: number) => number;
let sum: Sum = (a, b) => {
    a='hahaha';
    return a + b;
}
  • 推断规则也适用于结构化的存在(对象字面量)
const person = {
    name: 'zhanshen',
    age: 11
};
let name =person.name;
let age =person.age;
age = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型
  • 推断规则适用于解构
const person = {
    name: 'zhanshen',
    age: 11
};
let { name,age } = person;
age = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型
//数组也一样
const numbers = [1, 2, 3];
numbers[0] = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型

3.2 交叉类型

交叉类型(Intersection Types)是将多个类型合并为一个类型 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性

interface X {
    a: string;
    b: string;
}
interface Y {
    a: number;
    c: string;
}
type XY = X & Y;
type YX = Y & X;
//c = string & numbe

3.3 typeof

可以获取一个变量的类型

let p1 = {
    name:'zhanshen',
    age:10,
    gender:'male'
}
type People = typeof p1;

3.4 索引访问操作符

可以通过[ ]获取一个类型的子类型

interface Person{
    name:string;
    job:{
        name:string
    };
}
let FrontEndJob:Person['job'] = {
    name: '前端小白'
}

3.5 索引类型查询操作符 keyof

interface Person{
  name:string;
  age:number;
  gender:'male'|'female';
}
//type PersonKey = 'name'|'age'|'gender';
type PersonKey = keyof Person;

3.6 映射类型

在定义的时候用in操作符去批量定义类型中的属性

interface Person{
 name:string;
 age:number;
 gender:'male'|'female';
}
//批量把一个接口中的属性都变成可选的
type PartPerson = {
 [Key in keyof Person]?:Person[Key]
}
let p1:PartPerson={};
//也可以使用泛型
type Part = {
 [key in keyof T]?:T[key]
}
let p2:Part={};

3.7 条件类型

在定义泛型的时候能够添加进逻辑分支,以后泛型更加灵活

3.7.1 条件类型的分发

interface Fish { fish: string } 
interface Water { water: string }
interface Bird { bird: string } 
interface Sky { sky: string }
type Condition<T> = T extends Fish ? Water : Sky; 
let condition1: Condition<Fish | Bird> = { water: '水' }; 
let condition2: Condition<Fish | Bird> = { sky: '天空' };
  • 条件类型有一个特性,就是「分布式有条件类型」,但是分布式有条件类型是有前提的,条件类型里待检查的类型必须是裸类型参数(naked type parameter)

//none naked type //type Condition = [T] extends [Fish] ? Water : Sky; 简单说不是纯T的就不能分发

3.7.2 内置条件类型

TS 在内置了一些常用的条件类型,可以在 lib.es5.d.ts 中查看:

type R5 = Exclude<'a' | 'b', 'a'>  // 'b' (排除)
type R6 = Extract<'a' | 'b', 'a'>  // 'a' 	(提取)
type R7 = NonNullable<'a' | undefined>  // 'a' (去空)

3.7.3 infer 关键字,一个进阶用法中很重要的概念

表示在 extends 条件语句中待推断的类型变量

  • infer最早出现在此 PR
// 两个内置条件类型解析
// 获取函数 T 的返回类型
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;
// 获取函数 T 的参数的返回类型
type Parameters<T> = T extends ((...args: infer P) => any) ? P : never;

3.8 内置工具类型

TS 中内置了一些工具类型来帮助我们更好地使用类型系统

type Partial<T> = { [P in keyof T]?: T[P]; }; // 可以将传入的属性由非可选变为可选
type Required<T> = { [P in keyof T]-?: T[P]; }; // 可以将传入的属性变为必填项
type Readonly<T> = { readonly [P in keyof T]: T[P]; }; // 可以将传入的属性设置成只读
type Pick<T, K extends keyof T> = { [P in T]: T[P]; }; // 只取T中,只有K属性名的属性
type Record<K extends keyof any, T> = { [P in K]: T; }; // 将T所有属性值都映射到另一个类型上并创造一个新的类型

总结: 到这里,再去看看一些框架项目源码,有很多这种TS的进阶组合使用,当然TS已经帮我们内置了很多类型,足够我们平时99%的使用场景了