前言
为什么要记录下这个文档,也是说来话长。
本人算是全栈,后台出生,以前项目上使用过VB、ASP、C#、JAVA、JSP,对后台算是比较通。也用过Angular,Vue开发过大型项目,也曾深入了解过Vue框架的原理。
这年头的开发语言可能越来越高级,开发成本也越来越低。比如后台语言,以前用C的时候,真是欲仙欲死,现在例如JAVA等高级语言用了以后,发现程序员都不用关心底层,只要无脑的CRUD就行,什么内存管理都是帮你托管掉了。前端也是,以前哪个年代我们还用txt去编写html,现在用了Angular、Vue框架双向绑定以后,很多前端程序员连document.getElementByXXX这类找元素的方法都不知道了。10年前程序员还是个高科技的职业,现在被亲切的称呼为”码农“。
只能说,绝大部分企业都是利益至上的,一个需要十天半个月才能出成果,一个两天就出成果,当然选择两天的了。至于程序员个人素养,那时程序员自己的事情,关企业鸟事。
第一次用到TypeScript好多年前了,那时候基于Angular2+TypeScript开发过一套完整的应用系统。后续因为入门成本太高,公司决定转型Vue。
Angular1和Angular2及以上有本质的差别,这里不做赘述。
公司老的系统都是基于SpringMVC+Mybatis,然后前端是JSP。所以二级部门内是没有专门的前端(公司有前端部门,使用的也都是html+js这些),所以那时候是一帮后台人员做前端技术选型的。
当时公司整体技术选型不是太规范,一般都是二级部门为单位自己选型的。
因为我们是要构建一套大型的系统,在Angular2和Vue之间选型的时候,选择了Angular2,因为Angular2提供了一套完整的解决方案。经过验证使用以后,发现后台人员入门Angular2+TypeScript很轻松,因为TypeScript作者也是C#的作者,C#和JAVA高度相似。里面引入的什么依赖注入,模块化编程,面向对象对JAVA后台人员来说理解起来太轻松了,基于以前JSP积累的前端经验以及相似的后台经验,我们没过多久,就把完整的前端框架搭建出来。
部门内部自己用没什么问题,那时候部门抽出大概5,6个人。当然新技术还是需要有专研精神的人先搞,可以快速的把路趟平。
我们大概花了两个多月吧,把系统开发出来,基于自身切身的体验,说服领导,想推广到全公司。但是推广的过程很难受,也很复杂。
技术难度上:
说实话那时候公司的前端部门也没接触过三大框架(不是互联网企业,技术变革一直比较难),学习三大框架的理念本身就比较费心力。 中间让前端部门派了两个人跟我们一起学习使用Angular2+TypeScript,愣是学了一个多月没学会(没有完全全职学习),然后灰溜溜的回去了,后面就说这个推广价值不大。
其中一个后台部门光是学习这套框架就996集中开发了一个多月,他们是边做其他工作边学习,效果不是太好。学习这个技术还是要氛围的,我们那时候自己项目也忙,对别人只是提供技术支持的方式,没有系统的给他们培训,所以进度会比较慢,然后他们传出的声音也是这个推广价值不大。
我们可能高估了别人学习的环境和能力,也低估了技术本身的难度
架构使用难度上:
Angular2是一套有完整解决方案的框架,它不是那么灵活,可以随意搭配(当然搭配也可以,只是代价有点大),其他业务部门需要系统重构,才能应用。如果为了使用而使用,那又得不偿失了。如果新建项目使用, 其他部门内部可能也没有这么多人力去维护多套框架(当时我部门有三十多人,有充足的人员去维护)。
生产力难度上
说实话,国内市场Angular的开发人员确实太少了,挂牌了半年,愣是一个面试的都没有。 那时候Angular的资料国内网站很少,很多时候都是要“翻墙”去国外的网站上找,我刚开始用的时候,碰到一个技术难题,翻阅了很多源代码才找到解决方案。 大概折腾了一年吧,这套技术虽然我们内部用的很好,但是实际给公司能带来的很有限,还多了很多掣肘。并且因为公司开始通盘考虑统一公司整体的技术架构,方便后续能解决各部门之间的重复劳动。所以再一次进行了前端技术选型,这次是把公司相关人员都召集起来了,从人员、成本、技术难度等各方面做了评估,最终选择了Vue2做为统一的前端开发语言。
什么算是好的技术,我一直觉得能解决你的问题,能带来价值的,才是好的技术。不是最新的才是好的技术。Angular至少在我认为是没什么问题的,只是受限于当时的环境、人力物力,发现带来的价值远没有想象的这么好。Vue就不一样了,人员好招、入门简单、社区丰富、产生效果快。
当时就Vue2和Vue3进行过争论,那时候Vue3还没问世,但是有预览版了,我是看中他的TypeScript,静态类型检查等等,但是其他人统一不看好,认为这个不行,可能受过伤害吧。
天道有轮回,苍天饶过谁。这句话太对了,自Vue3发布以来,你会发现三大框架Angular/Vue/React已经殊途同归,越来越像了,Vue3也全面引入了TypeScript,然后公司发现前端还是要学习TypeScript。 我就笑笑,深藏功与名。
后面很长一段时间,我没怎么编写前端代码了,都是研究些前端原理,做一些前端的技术架构。也就是在去年,我们重组了前端的组织架构(中台、平台模式)以后,开启了内部分享(每两周一次,所有组员轮番上场分享,即是听众也是讲师),我是作为听众过去的,了解下每个人的沟通表达能力,技术能力。
有一期分享,就有员工介绍TypeScript了,我听了以后头很大,分享内容的算是一塌糊涂(可能有些概念对纯前端来说,确实很难理解,你以面向过程的方式去理解面向对象,本身就是存在问题)。也为了后期的考虑,我中间插了一期,专门介绍了下基于TypeScript的前端面向对象开发。
基于TypeScript的前端面向对象开发
这次不介绍基础概念,在我理解中,TypeScript就是加了“类型”的JavaScript,本身还是一种弱语言。
对我而言,他们缺少的是一些开发设计理念,而不是纯粹的编码,所以我重点也是培训开发理念,不讲基础。
代码地址:
类型
| 类型 | 基本类型 | 装箱类型 | |
|---|---|---|---|
| 字符串 | string | String | |
| 布尔值 | boolean | Boolean | |
| 数字 | number | Number | |
| 空 | null | ||
| 未定义 | undefined | ||
| 不可达 | never | ||
| 日期 | Date | ||
| 任意值 | any | ||
| 联合类型 | string | number | boolean |
| 数组类型 | string[] 或者 Array | ||
| 元组 | [string,number] | ||
| 类型体操 | type |
装箱类型其实就是基本类型的封装,但是要特别注意,在比较或者传值的时候,这两个属于不同的类型,要特别注意。
联合类型是其中比较特殊的类型,可以表达多种类型,有点泛型的感觉,只是类型框定了,我碰到的绝大部分业务场景,都是用泛型去替代了。很少用到联合类型(前端实际业务编码比较少了,可能没碰到过吧)。
类型体操也是我比较喜欢的一种东西,可以扩展各种自定义类型,后面有个章节会详细描述。
面向对象
概念也很简单。
- 类:类是一个模板,它描述一类对象的行为和状态。
- 对象:对象是类的一个实例,有状态和行为。
- 方法:方法就是行为,一个类可以有很多方法。
- 属性:每个对象都有独特的实例变量,对象的状态由这些实例变量的值决定。
比如把人抽象成一个类,那人的身高、体重、年龄、学历等都可以作为属性,然后计算BIM值(身高除以体重的平方)那就是方法。定义张三=new 人的实例化(),李四=new 人的实例化(),张三李四就是对象。661219
class 人 {
private 身高: number;
private 体重: number;
public 学历: string;
public 年龄: number;
// ...................
public getBMI(体重:number, 身高:number): number {
return 体重/ Math.sqrt(身高)
}
}
比如前端经常用的Axios访问后台服务的,也可以用Class的方式表达
const config = {
// 默认地址
baseURL: "",
// 设置超时时间
timeout: 5000,
// 跨域时候允许携带凭证
// withCredentials: true
}
class RequestHttp {
// 定义成员变量并指定类型
service: AxiosInstance;
// 构造体,实体化时触发
public constructor(config: AxiosRequestConfig) {
// 实例化axios
this.service = axios.create(config);
this.service.interceptors.request.use(
(config: AxiosRequestConfig) => {
// const token = localStorage.getItem('token') || '';
return {
...config,
// headers: {
// 'x-access-token': token, // 请求头中携带token信息
// }
}
},
(error: AxiosError) => {
// 请求报错
Promise.reject(error)
}
)
/**
* 响应拦截器
* 服务器返回信息 -> [响应拦截器] -> 客户端JS获取到信息
*/
this.service.interceptors.response.use(
(response: AxiosResponse) => {
const { data } = response
// 全局错误信息拦截
if (data.status && !data.access_token) {
if(data.status !== 200) {
return Promise.reject(data.msg)
}
else {
return Promise.resolve(data);
}
}
else{
return Promise.resolve(data)
}
},
(error: AxiosError) => {
const { response } = error;
return Promise.reject(response)
}
)
}
// 常用方法封装
get<T>(url: string, params?: object): Promise<ResponseBody<T>> {
return this.service.get(url, { params });
}
// 常用方法封装
getToken<T>(url: string, params?: object): Promise<UserInfo> {
return this.service.post(url, { params });
}
post<T>(url: string, params?: object): Promise<ResponseBody<T>> {
return this.service.post(url, params);
}
put<T>(url: string, params?: object): Promise<ResponseBody<T>> {
return this.service.put(url, params);
}
delete<T>(url: string, params?: object): Promise<ResponseBody<T>> {
return this.service.delete(url, { params });
}
getService(){
return this.service;
}
}
// 导出一个实例对象
export default new RequestHttp(config);
接口(抽象类)
接口(软件类接口)是指对协定进行定义的引用类型。针对第三方的调用,不用关心接口怎么实现的,只要调用接口就行,后续可以很方便的扩展,并确定遵循一定的标准。接口只有定义信息,没有实现过程,所以必须有具体的类去继承实现,单独接口是没有用的
抽象类大体和接口是类似的,就是抽象类可以定义抽象方法,需要被实现,也可以定义具体的通用方法,直接能使用。
接口
export interface ICommon{
getDataBase(): String;
getTableList(): Table[];
getColumnList(): Column[];
}
实现类1
export class MysqlCommonImpl implements ICommon {
//这些方法都是实现了接口的定义方法
getDataBase(): String {
return "mysql";
}
getTableList(): Table[] {
const tableList: Table[] = [];
for (let i = 0; i < 5 ; i++) {
const table:Table = {
tableName: "mysql" + i,
tableComment: "mysql表" + i
}
tableList.push(table)
}
return tableList;
}
getColumnList(): Column[] {
const columnList: Column[] = [];
for (let i = 0; i < 5 ; i++) {
const table:Column = {
ColumnName: 'mysql字段' + i,
ColumnComment: 'mysql字段描述' + i,
ColumnLength: 'mysql字段长度' + i,
ColumnPrecision: (i%2)==0?'mysql字段精度' + i:i
}
columnList.push(table)
}
return columnList;
}
}
实现类2
//接口可以被多次实现
export class OracleCommonImpl implements ICommon {
getDataBase(): String {
return "oracle";
}
getTableList(): Table[] {
const tableList: Table[] = [];
for (let i = 0; i < 5 ; i++) {
const table:Table = {
tableName: "oracle" + i,
tableComment: "oracle表" + i
}
tableList.push(table)
}
return tableList;
}
getColumnList(): Column[] {
const columnList: Column[] = [];
for (let i = 0; i < 5 ; i++) {
const table:Column = {
ColumnName: 'oracle字段' + i,
ColumnComment: 'oracle字段描述' + i,
ColumnLength: 'oracle字段长度' + i,
ColumnPrecision: (i%2)==0?'oracle字段精度' + i:i
}
columnList.push(table)
}
return columnList;
}
}
抽象类
export abstract class AbstractCommon {
getCommonInfo():string {
return "获取到公共信息了";
}
//方法必须被实现以后才能使用
abstract getChildInfo():String;
}
抽象类实现1
export class AbstractChildOne extends AbstractCommon {
getChildInfo(): String {
return "获取到了子类1的内容";
}
}
抽象类实现2
export class AbstractChildTwo extends AbstractCommon {
getChildInfo(): String {
return "获取到了子类2的内容";
}
}
继承、重载
对于统一类型类型的操作,可以把通用的方法封装到一起,作为父类,让个性化的操作作为子类。
子类继承父类,可以直接使用父类所有protected和public的方法,即使用通用方法;子类可以根据不同的业务定义自己个性化的属性和方法(这样可以实现共用和个性化)
比如编写一个人的父类,包含身高、体重等通用属性,包含计算BMI的方法,然后创建男人的子类,继承人的父类,这样男人子类就直接可以使用身高、体重、计算BMI等方法,男人子类也可以定义腹肌等属性、计算劳动力等方法,也可以某些特定场景下,重新定义计算BMI的方法(重写或者重载)
如果在父类方法不满足的情况下,可以对父类方法进行重载或者重写
- 同名、同参数方法覆盖 就是重写
- 同名,不同参数类型、个数、返回值等就是重载重载和重写都可以使用父类的属性
js前端的特性导致,前端没有重载的概念,只能重写。
一般常用的方法可以设置为静态方法,都能调用,但是实际业务很少用静态方法,可以对比下闭包的概念。
父类
export class BusinessService {
username?: String;
httpClient: HttpClient;
constructor(@inject(TYPE.HttpClient) httpClient: HttpClient) {
this.httpClient = httpClient;
}
/**
* 获取用户信息
* @param data
*/
getUserInfo(data:any): Promise<UserInfo> {
return this.httpClient.postToken(data);
}
getTemplateInfo(data:any, token:String): Promise<ResponseBody<TemplateInfo[]>> {
return this.httpClient.getTemplate(data, token)
}
protected protectedTest(){
return "测试是否能访问 protected方法"
}
}
子类
export class BusinessServiceExtend extends BusinessService {
getVideos(data:any, token:String): Promise<ResponseBody<VideoInfo>> {
return this.httpClient.getVideo(data, token)
}
getDataBase(dbType:DataBaseType): String {
const iCommon = this.getDataSourceImpl(dbType);
return iCommon.getDataBase();
}
getTableList(dbType:DataBaseType): Table[] {
const iCommon = this.getDataSourceImpl(dbType);
return iCommon.getTableList();
}
getColumnList(dbType:DataBaseType): Column[] {
const iCommon = this.getDataSourceImpl(dbType);
return iCommon.getColumnList();
}
protectedTest(){
return super.protectedTest();
}
getAbstractChildOneInfo():String{
const abstractCommon:AbstractCommon = new AbstractChildOne();
return `${abstractCommon.getChildInfo()} - ${abstractCommon.getCommonInfo()}`;
}
private getDataSourceImpl(dbType:DataBaseType):ICommon{
switch (dbType){
case DataBaseType.mysql:
return new MysqlCommonImpl();
case DataBaseType.oracle:
return new OracleCommonImpl();
case DataBaseType.pgsql:
return new PgsqlCommonImpl();
default:
return new MysqlCommonImpl();
}
}
}
泛型和Any
泛型简单来说也就是任意类型,咋看下和Any好像没有啥区别,实际上还是有差别的。
泛型:
- 通用类型,可以任意类型
- 可以做类型限定
典型使用场景,前端调用后台服务,后台服务一般有标准接口,比如包含status, msg,data, 其中data按照业务不同有 不同的结构,那data可以定义成泛型,可以是任意类型,但是实际接口返回的时候,可以限定泛型的类型。
Any:
- 通用类型,返回Object,基类
- 无法限定类型,也无法做出任何判断
从TypeScript的角度来讲,尽量少用Any,尽量用泛型做类型限定。
后台返回前端数据结构定义
export class ResponseBody<T> {
code: number = 0;
msg?: string;
success?: boolean;
//其中data就是泛型
data: T;
status?: number
constructor(data: T) {
this.data = data;
this.code = 200;
}
}
泛型类型限定
export class HttpClient {
constructor() {
}
postToken(data: any): Promise<UserInfo>{
const getTokenUrl = "xxxxx";
return service.getToken(getTokenUrl, data);
}
//这里返回ResponseBody<TemplateInfo[]> 就是表示对data泛型做了类型限定,比如是返回TemplateInfo[] 数组
getTemplate(data:any, token:String): Promise<ResponseBody<TemplateInfo[]>> {
service.getService().defaults.headers.common["X-Gisq-Token"] = "Bearer " + token;
const getTemplateUrl = "xxxxx/getAllTemplate";
return service.get(getTemplateUrl, data)
}
getVideo(data:any, token:String): Promise<ResponseBody<VideoInfo>> {
service.getService().defaults.headers.common["X-Gisq-Token"] = "Bearer " + token;
const getVideoUrl = "xxxxxx/video/findVideos";
return service.get(getVideoUrl, data)
}
}
枚举值
枚举是用户定义的类型,由多个通过逗号分隔的字符串常量组成。魔法值,固定类型等,都应该以枚举的形式表达
//数据库类型枚举值
enum DataBaseType {
mysql,
oracle,
pgsql
}
export default DataBaseType
依赖注入
传统模式下的类之间调用
class A {
void setData(){
}
}
class B {
void setData(){
const a = new A();
a.setData();
}
}
依赖注入模式下的类调用
@injectable()
class A {
void setData(){
}
}
class B(){
@inject(TYPE.A) a: A
void setData(){
a.setData();
}
}
依赖出入和控制反转一般是成双成对出现的。依赖注入是指由容器动态的将某个依赖关系注入到组件之中,比如B要调用A,那就由容器把A注入到B中去,控制反转就是这个意思,原来是在B中调用A,现在是把A注入到B中,方式整个反转过来了。
依赖注入主要是为了提升组件重用的频率(一般被依赖注入的对象会使用单例模式,即被初始化一次,当然这个也看具体的场景),并且能松耦合组件之间的依赖,调用方无需关心具体的被调用方来日哪里,怎么实现。
//定义 Symbol , 用于后入注入的时候定位
const TYPE = {
HttpClient: Symbol.for("HttpClient"),
BusinessService: Symbol.for("BusinessService"),
BusinessServiceExtend: Symbol.for("BusinessServiceExtend")
}
export default TYPE
//容器,用于管理所有注入的对象
const MyContainer = () => {
const myContainer = new Container();
myContainer.bind<HttpClient>(TYPE.HttpClient).to(HttpClient);
myContainer.bind<BusinessService>(TYPE.BusinessService).to(BusinessService);
myContainer.bind<BusinessServiceExtend>(TYPE.BusinessServiceExtend).to(BusinessServiceExtend)
return myContainer;
}
export default MyContainer
//injectable 表示对象支持被注入,这里要注意constructor构造体,如果是空的构造体注入比较容易,如果是带参数的构造体,注入就需要注意了。
@injectable()
export class HttpClient {
constructor() {
}
//.....
}
//本身注入被注入
@injectable()
export class BusinessService {
username?: String;
httpClient: HttpClient;
//构造体注入httpClient对象,容器会自动注入,不需要特殊处理
constructor(@inject(TYPE.HttpClient) httpClient: HttpClient) {
this.httpClient = httpClient;
}
}
<template>
<div class="hello">
{{ TestMsg }}
<div style="border: 1px solid red">
<HelloWordChild ref="HelloWordChild"></HelloWordChild>
</div>
</div>
</template>
<script lang="ts">
@Options({
components:{
HelloWordChild
}
})
export default class HelloWorld extends Vue {
TestMsg: string = "测试数据是不是正确";
//vue页面中通过这种方式注入,也可以直接使用
@inject(TYPE.BusinessServiceExtend) businessServiceExtend: BusinessServiceExtend = MyContainer().get<BusinessServiceExtend>(TYPE.BusinessServiceExtend);
constructor(props:any) {
super(props);
provide("msg", "测试子组件是否可以正确获取到注入的信息")
}
mounted() {
this.businessServiceExtend.getDataBase(DataBaseType.mysql)
//.........
}
}
</script>
装饰器
装饰器允许向一个现有的对象添加新的功能,同时又不改变其结构。这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能,可以提高代码的复用性,同时减少代码量。
在JAVA的概念中有一个注解的功能,两者的功效基本是一样的,针对方法,属性,类等做一些通用的处理,并且不影响本身的功能。
比如前端采集方法日志,可以就在方法上加上@Log日志装饰器,然后@Log装饰器里增加输入输出参数等获取,这样不要在方法里做任何埋点,可以通用处理掉。
//装饰器代码,PropertyDescriptor是核心属性,方法相关内容都在这里
function AgeJudge(target: Object,
propertyName: string,
propertyDescriptor: PropertyDescriptor) {
const method = propertyDescriptor.value
propertyDescriptor.value = function (age: number, age2: number) {
console.log("====================================")
console.log(`当前调用了方法1: ${propertyName}`)
// 检查是否是空字符串
if (age<100) {
throw Error('年龄1不能小于100')
} else {
console.log("当前方法入参(1):" + age);
console.log("当前方法入参(2):" + age2);
// 否则调用原来的方法
const result = method.call(this, age, age2)
console.log("当前方法返回值是:" + result);
console.log("====================================")
return result;
}
}
}
//装饰器代码
function Log(target: Object,
propertyName: string,
propertyDescriptor: PropertyDescriptor){
const method = propertyDescriptor.value
// @ts-ignore
propertyDescriptor.value = function (...args){
console.log("====================================")
console.log(`当前调用了方法2: ${propertyName}`)
for (let i = 0; i < args.length; i++) {
console.log(`当前方法入参(${i+1}): ${args[i]}`);
}
const result = method.apply(this, args);
if(result){
console.log(`当前方法返回结果是: ${result}`)
console.log("====================================")
return result;
}
console.log("====================================")
}
}
export class Autowired {
name: String;
tempValue: String = "";
constructor(name:String) {
this.name = name;
}
//装饰器1,校验年龄
@AgeJudge
getAge(age: number, age2: number): number {
return age + age2;
}
getName():String{
return this.name;
}
//装饰器2,日志处理
@Log
getText(param1:String, param2: number): String {
return `${param1} - ${param2}`
}
}
设计模式
设计模式是一种程序设计理念,在特定的场景用特定的设计模式,可以大大提升程序的扩展性和可用性,一般设计模式出现在后端语言中比较多,但是实际上设计模式可以运用到绝大部分语言中。
前面得到依赖注入,其实就是单例模式的一种应用(单例模式,类只会被初始化一次)。
代码简单展示工厂模式的使用(在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象)。
export class BusinessServiceExtend extends BusinessService {
getColumnList(dbType:DataBaseType): Column[] {
const iCommon = this.getDataSourceImpl(dbType);
return iCommon.getColumnList();
}
//简单的工厂模式,通过枚举值来区分具体的实现,外部调用只要传入枚举值就行
private getDataSourceImpl(dbType:DataBaseType):ICommon{
switch (dbType){
case DataBaseType.mysql:
return new MysqlCommonImpl();
case DataBaseType.oracle:
return new OracleCommonImpl();
case DataBaseType.pgsql:
return new PgsqlCommonImpl();
default:
return new MysqlCommonImpl();
}
}
}
class A {
@inject businessServiceExtend: BusinessServiceExtend
test(){
//传入枚举值就可以调用,不用关心后台如何分发及实现
const columnList = this.businessServiceExtend.getColumnList(DataBaseType.oracle);
}
}
类型体操
你如果了解类型了,其实类型体操不是太复杂,如果不太熟悉类型,可以类型体操就很难理解,严格意义来说,类型体操就是自定义类型,可以有很多的操作。
import {UserInfo} from "@/components/entity/UserInfo";
import * as ts from "typescript"
interface Eg1 {
name: string,
age: number,
}
interface Eg2 {
color: string,
age: string,
}
/**
* T的类型为 {name: string; age: never; color: string}
* 注意,age因为Eg1和Eg2中的类型不一致,所以交叉后age的类型是never
*/
type T = Eg1 & Eg2
const val: T = {
name: '',
color: '',
age: (function a() {
throw Error("当前类型冲突")
})(),
}
class Eg3 {
public name: String = "";
public age : number = 0;
}
/**
* Eg3是class类型,class和interface最大的区别就是 class对象需要初始化
*/
type T2 = Eg3;
const var2: T2 = {
name: "测试名称",
age: 0
}
class Eg4 {
name: String = "" ;
color: String = "";
}
class Eg5 extends Eg4 {
count: number = 0;
}
/**
* 类型继承
*/
type T3 = Eg5;
const var3: T3 = {
name: '123',
color: '123',
count: 1
}
interface Eg6 {
name1: String,
}
interface Eg7 {
name2:String
}
interface Eg8 extends Eg6,Eg7{
name3:String
}
/**
* 多继承
*/
type T4 = Eg8;
const var4: T4 = {
name1: "1",
name2: "2",
name3: "3"
}
interface Eg9 {
name: String,
text: String,
count: number,
}
type T5 = Eg9;
const var5: T5 = {
name: "123",
text: '123',
count: 1
}
type Partial2<T, K extends keyof T, F extends keyof T> = {
[P in keyof T as P extends K? P: never] : T[P]
} & {
[P in keyof T as P extends F? P: never] ?: T[P]
}
/**
* Partial2 类型体操,处理,name是必填的,text是选填的
* Partial2<Eg9, 'name', 'text'>; 其中name就是泛型T,text就是泛型F
* 第一句 [P in keyof T as P extends K? P: never] : T[P]
* 如果key是 name,则是必选的,返回 name: String(T[P]), 其他不处理(never)
* 第二句 [P in keyof T as P extends F? P: never] ?: T[P]
* 如果key是text,则是选填的,返回 text?:String(?: T[P])
*
*/
type T5Partial = Partial2<Eg9, 'name', 'text'>;
const var5Partial: T5Partial = {
name: '123',
}
type IsNumberOrBoolean<T> = T extends number ? { test:number }: never | T extends boolean ? { test:string }: never
/**
* 类型体操合一处理很多表示式,上面的就是如果是数字,则类型是 { test:number }
* 如果是boolean则类型是 { test:string }
* 下面因为 testType是boolean的,所以返回的T6NumberOrBoolean类型是 {test :String}
*/
const testType: boolean = false;
type T6NumberOrBoolean = IsNumberOrBoolean<typeof testType>;
const varT6NumberOrBoolean: T6NumberOrBoolean = {
test: '123'
};
type Unpacked<T> = T extends (infer R)[]? R: T
/**
* infer在类型体操里很有用的一个东西,就是自动类型推断
* 下面的场景就是自动推断数组里元素的类型
*/
const ids = [1, 2, 3]
const names = ['1', '2', '3']
//这里推断数字里元素就是number类型,所以定义1
type IdsType = Unpacked<typeof ids>
const varIds: IdsType = 1;
//这里推断数字里元素就是string类型,所以定义'123'
type NamesType = Unpacked<typeof names>
const varNames: NamesType = '123';
type Response<T> = T extends Promise<infer R> ? R: T;
/**
* 同理,这是对泛型类型进行自动推断
*/
type limitResponse = Promise<UserInfo>;
type resType = Response<limitResponse>;
const varResType: resType = {
user_tel: "",
user_name: "",
token_type: "",
user_id_number: "",
access_token: "",
user_kind: "",
refresh_token: "",
encrypt_str: "",
tenant_uid: "",
user_id: "",
tempToken: ""
}
type UnionType<T> = T extends { a: infer U; b: infer U } ? U : never;
/**
* JSON对象类型推断
* 如果JSON所有节点类型是一致的,那就返回一种类型
*/
type onlyString = UnionType<{a:string,b:string}>
const varOnlyString: onlyString = '123';
/**
* 如果JSON所有节点类型是不一致的,则合并同类项以后返回联合类型
*/
type unionType = UnionType<{a:number,b:boolean}>
const varUnionType: unionType = 1; //联合类型,可以定义number
const varUnionType2: unionType = false; //联合类型,可以定义boolean
type TupleToUnionType<T> = T extends(infer R)[]? R: never
/**
* 元祖转换成联合类型
*/
type tupleType = [string,number,Boolean];
type tupleToUnion = TupleToUnionType<tupleType>;
const varTupleToUnion: tupleToUnion = '1';
const varTupleToUnion2: tupleToUnion = 1;
const varTupleToUnion3: tupleToUnion = false;
/**
* 通过Type做加法运算,通过
* 数组的长度来计算,这个只是玩玩,没什么太大实际意义
*/
type 生成数组<填充数字 extends number, 数组长度, 已有数组 extends number[]> = 已有数组['length'] extends 数组长度 ? 已有数组 : 生成数组<填充数字, 数组长度, [填充数字, ...已有数组]>
type 数组<数组长度 extends number> = 生成数组<1, 数组长度, []>
type 加法<a extends number, b extends number> = [...数组<a>, ...数组<b>]['length']
type a =3;
type b= 5;
type D = 加法<a, b>
const value: 加法<a,b> = 8
总结
每个人对代码都有自己的理解,我也一直认为虽然语言很多,语法很多,但是大部分场景下,思想是一致的。后台也好,前台也好,只要你思路通了,久可以有各种各样的玩法,类似向设计模式这种思想,可以应用到很多前端的场合。不要让自己的思想局限了。
后来我用Java,C#, Python, C++, 甚至包括VB(直接用的Office里面的开发工具),发现我代码看起来好像一模一样,不管从文件定义,语法定义,结构定义等等。
TypeScript代码
Java代码
C#代码