基于TypeScript的前端面向对象开发

420 阅读17分钟

前言

  为什么要记录下这个文档,也是说来话长。

  本人算是全栈,后台出生,以前项目上使用过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,本身还是一种弱语言。

对我而言,他们缺少的是一些开发设计理念,而不是纯粹的编码,所以我重点也是培训开发理念,不讲基础。

代码地址:

github.com/assassinfym…

类型

类型基本类型装箱类型
字符串stringString
布尔值booleanBoolean
数字numberNumber
null
未定义undefined
不可达never
日期Date
任意值any
联合类型stringnumberboolean
数组类型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好像没有啥区别,实际上还是有差别的。

泛型:

  1. 通用类型,可以任意类型
  2. 可以做类型限定

典型使用场景,前端调用后台服务,后台服务一般有标准接口,比如包含status, msg,data, 其中data按照业务不同有 不同的结构,那data可以定义成泛型,可以是任意类型,但是实际接口返回的时候,可以限定泛型的类型。

Any:

  1. 通用类型,返回Object,基类
  2. 无法限定类型,也无法做出任何判断

从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#代码