业务开发实践TypeScript两三问

以下是之前公司内容分享的一篇文章,这是拿出来分享一样。

对于TypeScript相信肯定会有很多童鞋比我更熟悉、知识储备。所以,我这里更多是和大家交流下我对业务开发实践TypeScript遇到的一些问题,有些我有思考和解决办法,也有些没有,供大家一起讨论、思考。

为什么要用TypeScript?

  TypeScript减少Bugs数量?    

有些推荐Typescript文章,会有提到Typescript可以降低Bugs数量,真是这样么?    下面是我这几个月,按月统计的缺陷趋势(各位童鞋也可以看看自己的),毫无规律可言。

业务开发实践TypeScript两三问
所以,我还是坚持认为,业务复杂度以及排期时间充足与否,才是缺陷数量关键影响因素。即使是Typescript可以帮我们避免拼写错误,大多数情况下,就算不用TS,在开发、调试过程中我们也是可以发现的。
甚至,线上经常出现ReferenceError错误,可能大多数时候锅还得甩后端头上,后端接口不按约定返回,Typescript也解决不了。
说到这里,推荐访问后端返回的深层次的属性时,可以使用lodashget方法,可以一定程度上兜底解决引用问题。

// _.get(object, path, [defaultValue]) // 返回object指定path的值,如果是undefined,刚返回默认值  
var object = { 'a': [{ 'b': { 'c': 3 } }] }; 
_.get(object, 'a[0].b.c'); // => 3 
_.get(object, ['a', '0', 'b', 'c']); // => 3 
_.get(object, 'a.b.c', 'default'); // => 'default'

我们继续,下面是Typescript官网,关于Typescript可以解决什么问题的表述:

Typically, the need to ensure there are no bugs in your code can be handled by writing automated tests, then by manually verifying that the code works as you expect and finally having another person validate that it seems correct.

Not many companies are the size of Microsoft, however a lot of all problems writing JavaScript in large codebases are the same. Many JavaScript apps are made up of hundreds of thousands of files. A single change to one individual file can affect the behaviour of any number of other files, like throwing a pebble into a pond and causing ripples to spread out to the bank.

Validating the connections between every part of your project can get time consuming quickly, using a type-checked language like TypeScript can handle that automatically and provide instant feedback during development.

These features allows TypeScript to help developers feel more confident in their code, and save considerable amounts time in validating that they have not accidentally broken the project.

    我用翻译软件翻译了下,大概的意思是两点:

  1. 想要保证没有bugs,还是得自己写自动测试同时让测试童鞋测你代码。
  2. Typescript的功能,可以帮助你对你的代码更有信心,改动关联的地方的时候不用担心引起其它bugs——可以想到,这点明显还依赖于项目的Ts覆盖程度。

所以,Typescript对降低bugs作用我认为是不大的。
那,Typescript价值是什么?

TypeScript可以一定程度的代替文档

这点,对于写公共组件或方法时特别明显。就比如我们现在gerp-amazon-base导出的http实例。这个东西是完全没得文档滴。但是你只要点进去一看,你就可以知道有哪些方法,返回了什么。

export const http = {
  /**
   * methods return response.data
   */
  async get<T>(url: string,params?: any,config?: CustomRequestConfig): Promise<T> {
    const response = await requestService.get(url, {...config,params});
    return response?.data?.data;
  },
  async getResponse<T>(url: string,params?: any,config?: CustomRequestConfig): Promise<T> {
    const response = await requestService.get(url, {...config,params});
    return response.data;
  },
  async post<T, D = any>(url: string,data?: D,config?: CustomRequestConfig): Promise<T> {
    const response = await requestService.post(url, data, config);
    return response?.data;
  },
  async put<T, D = any>(url: string,data?: D,config?: CustomRequestConfig): Promise<T> {
    const response = await requestService.put(url, data, config);
    return response.data;
  },
  async delete<T>(url: string,params?: any,config?: CustomRequestConfig): Promise<T> {
    const response = await requestService.delete(url, {...config,params});
    return response.data;
  },
  /**
   * origin request
   */
  request: requestService.request,
};

但是对于我们业务来说,收益就直线下降了。现在前端的项目维护情况,大概是一个项目一个人维护,涉及改动也是这个人来。自己写的代码,就算没有Ts一样熟悉,连注释都不用写。至于接手的人怎么办,关我什么事?

以上,只是玩笑,大家别当真。所以,我认为,业务线写 Typescript 需要有一种利他的精神。当然,也并非完全利他,自己积极去尝试实践Typescript,至少还可以提升自己对Typescript的熟悉度,能够避免不变、体验和尝试一些新的东西,最重要的是也是简历上很好的一条技能

好,我就假设大家是有利他精神或者想提升自己Typescript的熟悉度。否则,我们就只能到这里就Game Over了,没法继续往下了。

在业务开发中,我们是否真做到深度实践TypeScript了,如何评判?

对于这个问题,我想我们大多数业务项目应该都是没有做到的,至少我接触的几个项目是都没有做到的。

怎么评判,最简单的办法,就是把tsconfig.json里的noImplicityAny属性设置为true,看看会不会多出来一堆错,真没错的童鞋,下次一定要来分享一下你们是怎么做到的。

之前,我理解noImplicityAny就是不能有any,不知道有没有童鞋是和我一样这么理解的。后面我才发现,自己的这个理解是错误的。noImplicityAny,只是不允许有隐式的any而已,也就是不能有Typescript无法推断出类型,而产生的any类型。你可以手动声明一个any就不会报错了。比如下面代码:

// 在ts会报错,不知道入参a是什么类型,所以是一个隐式的any
function aaa (a) {
    console.log(a);
}

TS Playground

区别在哪里?区别在于,未开启前,可能你并没有意识到,在处理的过程中,类型丢失了。手动声明成any,不管是不是偷懒,至少你是意识到了。

虽然,我们项目不能直接开启noImplicityAny。但,我们要认识到,想要尝试实践Typescript,就要有意识的尽量保证在处理的过程中,类型不丢失。

那么,如何保证类型不丢失?我想,使用泛型变量应该是我们保证类型不丢失必不可少的一种方式。这就引出泛型。

什么是泛型?为什么要有泛型?如何更好的理解泛型?

  什么是泛型?

  泛型(Generics),其实是强类型语言的概念或工具。比如,维基百科泛型在Java中的定义就是:泛型是泛型编程的一种工具。原文:Generics are a facility of generic programming.

  在TShandbook里,并没有给泛型下定义。我的理解,泛型就是类型变量,也就是类型声明空间的变量。

  为什么要有泛型?

A major part of software engineering is building components that not only have well-defined and consistent APIs, but are also reusable. Components that are capable of working on the data of today as well as the data of tomorrow will give you the most flexible capabilities for building up large software systems.

In languages like C# and Java, one of the main tools in the toolbox for creating reusable components is generics, that is, being able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.

这是TShandbook对泛型最开始的一段表述,大概的意思是:在构建大型软件系统时,可以支持各种类型的数据的组件具有最好的可扩展性。泛型允许消费都传入自己类型去消费组件。
其实,也就是为了增加类型系统的灵活性同时又不丢失静态类型检查的功能。
比如:

// 下面是一段业务常用了,将一个选项列表转换成map结构
// 以方便快速通过指定有值获取对应的列表项
const listToMap = (data, key = 'value') => {
  let mapObj;
  (data || []).forEach((item) => {
      mapObj[item[key]] = item;
    });
  return mapObj;
};

const options = [
    { label:'我是值1', value:'value1' },
    { label:'我是值2', value:'value2' },
 ]
 
 const optionsMap = listToMap(options);
 console.log(optionsMap['value1']) // 打印出了 { label:'我是值1', value:'value1' }

 // 为了转换后类型不丢失,我们需要对类型进行约束
 // 改写成下面这样
type Option = (typeof options)[number];
type OptionMap = {[propName:string]: Option};
const tsListToMap = (data:Option[], key = 'value'): OptionMap => {
  let mapObj:OptionMap={};
  (data || []).forEach((item) => {
      mapObj[item[key]] = item;
    });
  return mapObj;
};

const optionsMapByTs = tsListToMap(options);

// 但你会发现,突然我有个列表,列表项不是Option结构,如
const contents = [
    { content:'我是值1', key:'value1' },
    { content:'我是值2', key:'value2' },
 ]
// 我们依然可以使用 listToMap去做转换,但不能使用tsListToMap去了,contents不符合Option[]类型

TS Playground – An online editor for exploring TypeScript and JavaScript

如何更好的理解泛型?

想要更好地理解泛型,我们需要理解,在TS中存在两种声明空间:类型声明空间与变量声明空间。

类型声明空间,包含用来当做类型注解的内容,例如下面的类型声明:

class Foo {};
interface Bar {};
type Bas = {};

你可以将 Foo, Bar, Bas 作为类型注解使用,示例如下:

let foo: Foo;
let bar: Bar;
let bas: Bas;

变量声明空间,包含可用作变量的内容,示例所示:

class Foo {};
const someVar = Foo;
const someOtherVar = 123;

这里需要注意,Class会类型声明空间和变量声明空间都声明一个变量。如上面代码的Foo,你即可把他当成一个类型变量,也可以当成一个普通变量。

我们类型志明空间的内容,不能在变量声明空间使用,反之亦然。如:

interface Bar {}
const bar = Bar; // Error: "cannot find name 'Bar'"

const foo = 123;
let bar: foo; // ERROR: "cannot find name 'foo'"

TS Playground

但可以使用typeof根据变量声明空间的内容,生成类型声明空间内容。如:

const foo = 123;
let bar: typeof foo; // 这里bar的类型是什么?
//  ^? 
    

TS Playground

要理解,TS是静态检查,所以在做类型编程时,我们是不可能也不应该从一个动态的变量来获得精确的静态类型,比如 as const断言只能用于字面量,不能用于变量。

更好地使用TypeScript的一些小知识点

通过枚举类型结合类型运算,更好的约束我们的枚举值

export type TagType = 'campaign' | 'product' | 'target'; 

export const tagTypeMap: {
  [K in TagType]: {value: K; label: string};
} = {
  campaign: { label: 'label1',value: 'campaign'},
  product: { label: 'label2',value: 'product'},
  target: { label: 'label3',value: 'target'},
};

这里,TagType可能用于约束接口参数类型、组件参数类型;tagTypeMap消除魔术字符串和返回label。通过使用in运算,我们可以很好绑定约束类型。后续如果需要添加其它枚举,一定需要在两个地方都添加,否则就会报错,可以避免遗漏。

基于类型类型快速生成新的类型

export interface PmMarketItem {
  storeName: string;
  marketInfos: {
    marketId: number;
    marketName: string;
    countryId: number;
    countryCode: string;
    marketplaceId: string;
  }[];
}

// 这里 方括号类型运算符 获得 PmMarketItem的marketInfos属性的元素类型
type PmMarketItemMarket = PmMarketItem['marketInfos'][number];

// 使用interface的extends关键字快速生成新的类型
interface ChildrenItem extends PmMarketItemMarket {
  label: PmMarketItemMarket['marketName'];
  value: PmMarketItemMarket['marketId'];
}

我们还可以使用TypeScript的类型工具,如:Pick<Type, Keys>Omit<Type, Keys>Partial<Type>等快速生成新的类型。更多类型工具,可以查看TypeScript 类型工具

接口字段太多了,添加精确类型太麻烦了怎么办?

  在业务线的开发过程中,其实更多的类型还是来自于后端接口,一旦对接口出入参进添加了精确的类型声明,会大大提高我们代码TypeScript的覆盖率,对于出参也可能有更好的类型提示。

  但是,当接口字段太多时,想要严格添加类型约束,确实有些麻烦。为了解决这个问题,写了一个EolinkApi2TsType插件。如图,可以直接eolink上的接口,解析成精确的ts类型。详情使用方式可以查看 eolink-api-2-ts-type

业务开发实践TypeScript两三问业务开发实践TypeScript两三问

主动拥抱、使用TypeScript近一年的时间,我收获了什么?

从今年开始,我们的项目基本都添加了TypeScript相关配置,并鼓励我们使用TypeScript。这一年,对于TypeScript我是主动拥抱了,那么,使用TS一年到底有什么收获呢?我总结有以下几点:

  1. 是真正的提升的类型思维、也加深了对TypeScript的理解及熟练度;

  2. 在可以快速声明接口出参类型的前提下,使用TypeScript确实给我带来了更友好、高效的提示,也避免了一些很简单的问题上抽风花费时间。比如:

        export type TagType = 'campaign' | 'product' | 'target'; 
    
        export const tagTypeMap: {
          [K in TagType]: {value: K; label: string};
        } = {
          campaign: { label: '广告活动',value: 'campaign'},
          product: { label: '商品',value: 'product'},
          target: { label: '投放',value: 'target'},
        };
    
        const activeTab = ref<TagType>(tagTypeMap.campagin.value)
        ```
    

基于上述代码,某些时候,我们很可以会抽风写出下面这样的判断。如下:

activeTab.value === tagTypeMap.campaign

然后又花费半小时或者个把小时的时间去排查为什么程序运行不对,最后发现只是tagTypeMap.campaign少了.value后猛拍大腿或骂自己傻X。

但如果activeTab.value和tagTypeMap都精确的声明的类型,TypeScript会直接提示等号两边永远都不可能相等,可以为我们省去很多时间。

  1. 我确切的感受到了在修改一些类型约束很完整的代码时,TypeScript所带来的健壮性,值得我们花费相应的时间成功去进一步提高业务代码的TypeScript和覆盖率。

参考:

  1. Why does TypeScript exist?
  2. Lodash Documentation
  3. en.wikipedia.org/wiki/Generi…
  4. Vue 3.3 正式发布,代号:Rurouni Kenshin

原文链接:https://juejin.cn/post/7347678142176542720 作者:前端胖子

(0)
上一篇 2024年3月19日 下午4:32
下一篇 2024年3月19日 下午4:42

相关推荐

发表回复

登录后才能评论