三、ts 中的类型安全、里氏替换原则、协变和逆变、联合类型转换为交叉类型

一、前言

所有的编程语言都可以分为两种:

1. 编译型(最大的特点就是强类型)

diff 复制代码
- java (主要用于服务器端,市场占有率比较高) 
- c (非常经典的面向过程的编程语言,性能高)
- c++ (在C的基础上,加入面向对象的思想,在很多领域都有广泛的应用)
- C# (游戏开发、服务器端和java竞争, 由于后生优势发展势头好。)
- GO (原生并发支持比较好,高性能 )
- ...

2. 解释型(弱类型)

scss 复制代码
-  **javascript** (本文的使用率非常高,大多数浏览器都支持,主要用于前端,nodejs把它带入了后端,前端也能卷后端了,哈哈哈·····)
-  python(大数据、爬虫、自动化脚本、web开发 也是非常火爆)
-  php(号称世界上最好的编程语言,前后端不分离?jsp ? 我看谁还敢说我不是最好的语言)。

二、ts 基本的基本情况

为什么引入ts?为什么要使用ts?

这个问题争论很大,比如前段时间Svelte接受采访时说放弃 使用ts, 转而使用jsDoc。 (放弃使用ts的理由)。 TypeScript 不是那种你可以直接放在现有工作流程之上然后就忘记的工具。正如凯尔·谢夫林 (Kyle Shevlin) 在 2019 年写的那样......

我今天的工作流程:用 15 分钟编写可以运行并实现我想要的功能的代码。2个小时试图安抚静态型神。😕

有人享受ts带来的快乐(不会用ts是你的问题,haha~),也有人在承受 安抚ts类型带来的痛苦

个人看法: 我并非大佬,也饱受ts的折磨,但是有时候ts也让我感受到了方便。目前我还只是一颗螺丝钉,并没有自行决定是否使用ts的权力。就目前来说,还是有很多公司在使用ts,作为一名工程师,我们有义务和别人互相兼容,迎难而上。 诚然,ts让我承受了一定的痛苦,但是学习过程中也让我进一步学习了类型理论的知识。

于是有了这篇文章,正文开始:

三、类型安全

ts 在设计过程中并没有标新立异,也许细节上有一些出入,但在整体上就像其它编译型语言一样,遵循了同一套类型理性系统 。所谓类型安全,其本质就是让ts的设计符合这套类型理论系统的 类型赋值规则 ,不满足类型赋值规则 的操作需要报错进行提示,让开发者在开发阶段就找到代码存在的bug。

举个栗子:

先看一段js 代码:

javascripts 复制代码
    let myAge = 18  
    let myName = 'Rock'
    myAge = myName

js 语法上没有任何的问题,但是如果是C语言或者java 语言中呢?

再来看一段 java 代码:

java 复制代码
    int  myAge = 18;
    String myName = "Rock";
    myAge = myName; // 这里一定会导致类型报错 TypeError

ts 加入类型限制以后,也需要实现类似的功能,没错,这就是类型安全!

typescript中:

ts 复制代码
    let myAge:number = 18  
    let myName:string = 'Rock'
    myAge = myName // 类型报错 TypeError

当然,这只是最简单的类型安全,还有其他的类型安全规则,比如非常著名的 里氏替换原则

四、里氏替换原则(面向对象中多态的一种)

里氏替换原则:如果一个类Son是另一个类father的子类,那么在任何使用类Father的地方都可以用类Son的实例来替换而不产生任何错误或异常。所有面向对象的编程语言都遵循了这里原则。反过来就需要抛出类型报错,这其实也是一种类型安全。

举个栗子:

比如:写过java后端的小伙伴肯定知道,service 层的所有的实现都实现了对应的接口。 这就是 100% 遵循里氏替换原则的最好实践。

声明一个接口IUserService ,假设接口可以实例化的话,那么IUserService 接口正常需要指向自己的实例化对象。但是其实接口不能实例化,接口生而为父。 所有需要使用接口的实例化对象的地方,都可以用相应的子类的实例化对象去替换。 来一段Java 代码

java 复制代码
// IUserService.java
public interface IUserService {
    boolean login(User user);
}
// UserServiceImpl.java
public class UserServiceImpl implements {
    boolean login(User user){
        // 处理业务。。。。。
        // 查询数据的对应的密码
        // 加密user.password
        // 对比是否相等 ,返回对应的结果等等
    }
}

//UserController.java 部分代码省略
public class LoginController {
    private IUserService userService = new UserServiceImpl();
    public boolean login(@RequestBody User user) {
        // 登录
        return userService.login(user);
    }
}

核心就一个地方 private IUserService userService = new UserServiceImpl();

这里其实就是里氏替换原则的使用。所有的子类的实例都可以替换父类的实例。ts 也遵循这套规则,因此也可以这样操作。

ts 复制代码
interface IFather {
    money: string;
}

interface ISon extends IFather {
    house: string;
    drink: (address: string) => void;
}

class Father implements IFather{
    money = '¥0'
    constructor(money) {
        this.money = money
    }
}
class Son extends Father implements ISon{
    house='贵州大学'
    drink=(address)=>{
        console.log(`我在${address}喝酒`)
    }
    constructor(house,money) {
        super(money);
        this.house = house
    }
}

let son = new Son('贵州大学','KTV')

let father = new Father('¥100000000')
// 不报错
father = son
// 报错 TS2739: Type 'Father' is missing the following properties from type 'Son': house, drink
son = father

可以看到,可以替换father,但是father 不能替换son

里氏替换原则(个人大白话,有错欢迎指出):

先确两个概念:抽象具体

  • 抽象: 越靠近父类越抽象的(信息越少,能兼容的后代越多)Animal是比较抽象的,但Object 是最抽象的!
  • 具体: 越靠近子类越具体的(信息越多,能兼容的后代越少)Dog是比较具体的。slimDog(细狗) 是最具体的。

里氏替换原则

  1. 需要被赋值的对象(指针)越抽象越好(等号的左边,函数的形参),这样可以让他接受非常多类型的实例对象。
    • Father 类型比Son 抽象 ,Father 更加合适作为 等号的左边,函数的形参。
    • son 的实例比father 的实例更具体,信息更多,更适合用来赋值,(等号的右边或者作为函数返回值)
  2. 也就是说,定义函数的时候,形参需要更加抽象,返回值需要更加具体。给函数重新赋值的时候不能违背这一规则。

利用这个特性,就可以开始解释协变和逆变了 :

五、 协变和逆变

函数的参数会发生逆变,返回值会发生逆变。 举个栗子

ts 复制代码
// 参数需要很抽象,越抽象越好  
let fFunc = (person: Father) => {  
console.log(person);  
};  
fFunc(father); // ok  
fFunc(son); // ok  
//其实本质就是赋值,还是那句话,所有需要父类实例对象的地方,都可以用子类去替换  
let sFunc = (person: Son) => {  
console.log(person);  
};  
sFunc(son); //ok  
//sFunc(father); //errorArgument of type 'Father' is not assignable to parameter of type 'Son'.   Type 'Father' is missing the following properties from type 'Son': house, drink  
  
// Error  
fFunc = sFunc; // 函数的输入要尽可能宽松(尽可能使用父类型),不能使其缩小  
  
// ok 逆变 参数的输入可以变得更加的宽松  
sFunc = fFunc;  
  
// 返回值需要很具体,越具体越好  
let fFuncReturn = () => father;  
let sFuncReturn = () => son;  
  
// OK 协变 返回值需要变得更加具体  
fFuncReturn = sFuncReturn; // 变得更加具体了,可以获取的信息变多了 ok(信息要尽可能变多,不能变少,或者说要变得更加具体)  
  
// Error  
sFuncReturn = fFuncReturn; // 返回值变得更加抽象了,不行!!!(信息丢失了)

可能还是有点模糊,再举个栗子

ts 定义一个定义一个类型。判断T是否是U的子类型

type MyIsSubType<T, U> = T extends U ? true : false;

type T1 = MyIsSubType<1, number>; // true

使用上面的四个函数进行两次试验:fFuncsFuncsFuncReturnfFuncReturn

  1. 逆变
ts 复制代码
type T2 = MyIsSubType<typeof sFunc, typeof fFunc>; // false 逆变  
// Error  
let B2: T2 = true; // Type 'true' is not assignable to type 'false'.  
 // 交换位置
type T3 = MyIsSubType<typeof fFunc, typeof sFunc>; // true 逆变  
// OK  
let B3: T3 = true; 

可以看到,typeof fFunctypeof sFunc 的子类型,这就是逆变

  1. 协变
ts 复制代码
//协变  
type T4 = MyIsSubType<typeof sFuncReturn, typeof fFuncReturn>; // true 协变  
// error  
let B4: T4 = false; //Type 'false' is not assignable to type 'true'.  
let B5: T4 = true; //OK

可以看到 typeof sFuncReturntypeof fFuncReturn 的子类型,这就是协变.

六、 联合类型转换为交叉类型

  1. 利用逆变(infer)把联合类型转换为交叉类型(函数参数位置的infer推导的组合方式就是&),在 TypeScript 的这个 PR中有一句话:

multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred.

翻译过来就是: 同一类型变量在逆变的位置上的多个候选会导致推断出交叉类型。

ts 复制代码
// 联合类型转换为交叉类型  
type U2I<U> = (U extends any ? (K: U) => void : never) extends (K: infer T) => void ? T : never;  
  
type U = U2I<{ name: string } | { age: number }>;  
type U = U2I<{ name: string } & { age: number }>;
// 上面两行代码执行结果一样
const test33: U = {  
    name: 'Rock',  
    age: 18,  
};
相关推荐
Tiffany_Ho5 小时前
【TypeScript】知识点梳理(三)
前端·typescript
看到请催我学习9 小时前
如何实现两个标签页之间的通信
javascript·css·typescript·node.js·html5
天涯学馆16 小时前
Deno与Secure TypeScript:安全的后端开发
前端·typescript·deno
applebomb19 小时前
【2024】uniapp 接入声网音频RTC【H5+Android】Unibest模板下Vue3+Typescript
typescript·uniapp·rtc·声网·unibest·agora
读心悦1 天前
TS 中类型的继承
typescript
读心悦1 天前
在 TS 的 class 中,如何防止外部实例化
typescript
Small-K2 天前
前端框架中@路径别名原理和配置
前端·webpack·typescript·前端框架·vite
宏辉2 天前
【TypeScript】异步编程
前端·javascript·typescript
LJ小番茄3 天前
TS(type,属性修饰符,抽象类,interface)一次性全部总结
前端·javascript·vue.js·typescript
It'sMyGo3 天前
Javascript数组研究03_手写实现_fill_filter_find_findIndex_findLast_findLastIndex
前端·javascript·typescript