一、前言
所有的编程语言都可以分为两种:
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(细狗)
是最具体的。
里氏替换原则
- 需要被赋值的对象(指针)越抽象越好(等号的左边,函数的形参),这样可以让他接受非常多类型的实例对象。
Father
类型比Son
抽象 ,Father
更加合适作为 等号的左边,函数的形参。son
的实例比father
的实例更具体,信息更多,更适合用来赋值,(等号的右边或者作为函数返回值)- 也就是说,定义函数的时候,形参需要更加抽象,返回值需要更加具体。给函数重新赋值的时候不能违背这一规则。
利用这个特性,就可以开始解释协变和逆变了 :
五、 协变和逆变
函数的参数会发生逆变,返回值会发生逆变。 举个栗子
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
使用上面的四个函数进行两次试验:fFunc
、sFunc
、sFuncReturn
、fFuncReturn
- 逆变
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 fFunc
是 typeof sFunc
的子类型,这就是逆变。
- 协变
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 sFuncReturn
是 typeof fFuncReturn
的子类型,这就是协变.
六、 联合类型转换为交叉类型
- 利用逆变(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,
};