Typescript 在实际开发的应用

3,295 阅读34分钟

前言:

        随着前端技术的发展, 老的技术,已经不能给大家带来满满的装b感了。 谁能掌握前沿技术,谁就是前端程序员中,那个最靓的仔。那么前端技术发展,其潮流已经开始慢慢的拥抱TS。从现在的框架、轮子, 以至于以后的业务代码,TS都会越来越流行。vue2 对ts的支持并不是特别好,所以从vue3开始,尤大佬也开始使用TS。虽然从现阶段来看,有些地方优化并不是很好,但是我相信,不久的将来,这些问题将都不是问题,并且很有可能在未来的浏览器上,直接运行ts。所以ts我们可能现在用不上,但是确是一门不能不学的技术。

我们为什么要使用TS

       作为弱类型语言的使用者,我们刚开始接触ts的时候可能会有些不适,我们习惯了把一种类型,在代码中突然变成另一种类型,习惯了再对象中插入或者删除属性。但是我们并不习惯给变量加上条条框框,不习惯在刚开始使用ts的时候,满屏飙红的那种抓狂感觉。但是当你真正习惯并熟练使用的时候,飙红就不存在了。 你会发现,你写代码可以盲写,不用写一行,刷新一下浏览器,console.log(xx) 一个变量了。 等你风风火火的写完一段逻辑需求的时候,一刷新页面,发现竟然功能正常,没有报错。这种感觉就两个字,真香。

       我们可能觉得,书写声明文件,时刻注意数据类型,会徒增很多工作量。 这个确实是这样。 但是从长远角度来讲,必然是利大于弊,提高了轮子的复用性,提高了代码的可读性,稳定性,可维护性。 比起增加的那点工作量,简直就是可以忽略不计。

       这里我就不讲什么大道理了。 从我们真实的开发角度来讲,作为开发者,肯定维护过以前的老旧代码。每个人的规范不统一,能力参差不齐,导致你看到的代码,各种奇葩。你在迭代或者修改的过程中,出现各种莫名其妙的bug。 相比以下这种错误我们很常见吧?

image2021-7-15_9-48-0.png

image2021-7-15_9-48-12.png

错误不可怕,可怕的是测试正常,上线后,因为种种特殊的原因,数据结构发生变化,导致特定情况无法访问变量从而出现的错误。

那么这些错误出在什么地方呢?就出现在类型不严谨,变量使用不规范的情况下。

下面这样的代码,想必大家都见过吧?

const Obj = await ajax.get('xxx')
Obj.props1 = []
Obj.props2 = {}
delete Obj.data
Obj.getData()
/* 或者 */
let isLoad = false
isLoad = 0
isLoad += '0'
isLoad = Number(isLoad)
if (isLoad) {
  isLoad = {}
} else {
  isLoad = []
}

我们随意修改数据的类型,如果代码逻辑一旦复杂,并且操作一旦过多,就会导致,使用时候的类型,和声明的时候不一致。从而在一些特殊情况下,导致变量无法访问,或者本该调用的函数,变成了undefined 本该调用的对象,变成了其他类型导致提示 TypeError

那么,我们使用TS ,结合ide工具,在一开始,就能避免这些问题。 接下来,我会从,项目开发的角度触发,着重的去讲讲,TS在实际项目开发中的使用。 TS的基础我就不讲了,所以如果对ts不了解的,建议提前看下TS的基础文档

TypeScript在项目应用中的使用

一、类型声明

在实际的开发中,除了 一些基础类型,例如:number string boolean之外,我们还会使用到一些稍微复杂的类型,比如 对象的类型, 数组的类型, 函数的类型,以及特殊约定类型等

那么我们看看这些类型,在实际开发中是如何应用的。

1. 对象的类型 interface

在使用js开发的过程中,对象是我们使用非常广泛的一种类型。我们可以使用 interface 去声明一个对象的类型。比如我们从后台获取到数据的时候


interface DataType {
	id: number
	act_title: string
}
const Data: DataType = await ajax.get('/api/getData')

这里,我们从ajax获取的数据,都是any,因为编译时,ts并不知道,我们使用这个接口将会获取到什么数据,所以我强烈建议大家,对着文档,将后台返回的数据类型进行声明。这里的数据是所有数据的源头,只有这里把握好了,那么才能为后面的开发奠定基础。 当然这时候,肯定有人会问,那后台的返回的数据不固定怎么办,比如act_title有时候是 string 有时候是number, 返回的data有时候是对象,有时候是数组,这种情况怎么办?  我告诉你怎么办, 你直接对后台开发说,你知道接口的含义吗,你知道接口的类型需要固定吗?你知道。。。。 然后把他怼回去,让他去改代码。一定要遵循一个标准,就是,后台返回的数据类型,永远都是固定的。 可以酌情,在某些情况下,缺少一些属性。但是一旦有这个属性,那么这个属性的类型,必须是确定的。

interface是具有嵌套性的,比如:

interface DataType {
    id: number
    act_title: string
    theme: {
	color: string
        fontSize: number
    }
}
const Data: DataType = await ajax.get('/api/getData')

你也可以写成

interface ThemeType {
    color: string
    fontSize: number
}
interface DataType {
    id: number
    act_title: string
    theme: ThemeType 
}
const Data: DataType = await ajax.get('/api/getData')

这样写的好处是, ThemeType 这个类型,如果你在后面可能用到的时候,可以直接使用。 上面的ThemeType是匿名的写法,下面为具名的写法。这点,和js的语法是一个道理。 当然并不是说,我们使用上面匿名的写法,就没法直接使用ThemeType的类型。 我们如果要用到theme的类型 的时候,又不想声明ThemeType ,那可以使用 ,这个类型就是 theme 的类型, 和后面代码块中声明的ThemeType 是相同的。但是看起来肯定不好看么。 其次肯定就有人要问了, 那能否用DataType.theme去代替DataType['theme']? 答案是不行的。 用“.”是命名空间的写法,后面我会讲到

2. 数组的类型之一 Array泛型

数组也是我们在开发中常用的类型,我们在展示列表的时候,经常会使用到数组,并进行操作。


const Data: Array<{ id: number }>= await ajax.get('/api/getIds')  // 我们使用数组泛型来声明每一项都为 { id: number } 的数组,这个{ id: number }是匿名的interface写法, 你也可以自己去声明一个 interface 把其类型名称放在尖括号里面。 至于泛型,后面再讲
// 或者使用 类型[] 的方法声明数组类型

const Data: string[]= await ajax.get('/api/getStrings') // Data的类型为 一个每一项都是字符串的数组

当你声明一个变量为数组的时候,你如果在后面 敲 ‘.’ ide就会提示出所有关于数组的方法, 比如 map  forEach  som includes 等,这些提示在开发的时候会非常的友好,如果没有提示这些方法,那很有可能你这个数据存在问题,即便强行输入,也会报错。 同理对象也是一样的, 你在声明了类型的对象后面敲 ‘.’ 就会提示出,这个对象中所有被声明的属性 并且我们在 Array中可以嵌入 interface , 同理在 interface中也可以嵌入 Array,比如:



interface ListItem {
    id: number
    name: string
}
interface DataType {
    total: number
    list: Array<ListItem>
}
const Data: DataType = await ajax.get('/api/getData')

3. 数组的类型之二  元组

Array泛型是声明一个长度不定,数组每项内容类型固定的数组。  那么如果我们使用长度固定,每项内容一样(或者不一样)的数组的时候,如何声明其类型呢。 这时候就需要使用到元组类型了。 元组的格式以及其中的内容是固定写死的,例如


type TupleType = [string, string, string, string, string, string, string]
const weekEcum: TupleType = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日']
weekEcum[0] // 星期一
weekEcum[7] // 报错  因为元组是有固定长度的,该数组并没有第八项

我们还可以声明每一项不同的元组


type TupleType = [number, string, boolean]
const shop: TupleType = [ 11250, 'xxx官方旗舰店', true] // 店铺id   店铺名称  店铺是否开通会员通
shop[0] // 返回的数据为 number
shop[1] // 返回的数据为 string
shop[3] // 报错  因为元组是有固定长度的,该数组并没有第4项

元组可以约定一个 数组项之间可以存在不同的情况。 也可以限制数组的长度。 我们在使用中,可以更加精确的控制变量的类型。 就比如,每周的天数,最多只有7天。 我们在声明类型的时候,就限制长度,避免我们在开发的过程中,遇到越界访问的情况。

4. 函数的类型

在js中,函数也是属于变量, 任意一个函数的类型,我们可以使用自带的 Function去表示, 但是这个和any又有什么区别呢? 所以我们在声明函数的时候,还是需要将其入参,以及返回值进行限制,从而避免调用出错等情况。



type GetDataFun  = (index: number) => string
const getData: GetDataFun = (index: number) => {
    const str = '使用typescript,天天快乐'
    return str[number]
}

从上面的代码可以看出,我们声明了一个GetDataFun 类型,它是一个函数的类型, 接收一个 number的参数,返回一个string。  我们在声明函数的时候, 将等号右边的赋值给等号左边的。等号左边的是你声明的类型, 那么如果等号右边的函数体,和你声明的类型不符,就会报错


type GetDataFun  = (index: number) => string
const getData: GetDataFun = (index: number) => {  // 报错会说, 不能将 (index: number) => number 赋值给 (index: number) => string
    return index + 1
}

从上面可以看到,等号右边的类型,实际上是接受一个 number参数, 返回一个number, 所以和你声明的 GetDataFun   所以就会报错。 至于等号后面函数体的类型,是如何出现的呢? 这个就属于ts的类型推导了。 即便你没有声明变量体的具体类型, 但是ts会根据当前变量体推导出其最大可符合的类型(比如 let a = 1; a的默认类型就是number 虽然你没有声明,但是ts根据 = 右边的值来推导出来的), 这里需要注意的是,最大可符合的类型。  看看下面的代码

type GetDataFun  = (index: number) => [string, string, string]
const getData: GetDataFun = (index: number) => { // 会报错, 因为函数体返回值的类型(Array<string>), 和函数声明的类型的返回值类型不符  [string, string, string]
    const classMember = [['李建成', '李世民', '李元吉'], ['李治', '李承乾', '李泰']] // classMember 推导出来的类型, 并不是 一个元组, 你理想的是 [[string, string, string], [string, string, string]],但实际上是 Array<Array<string>>
    return classMember[index]
}

从上面可以看出。ts根据值推导出的类型,是最大可符合的类型,而并不是更加精确的类型, 所以我们在使用的时候一定要注意。 我们在匿名函数声明的时候,如何设置其类型呢


const getData = (index: number): number => {
return index + 1
}

const getData = function (index: number): number {
    return index + 1
}

function getData (index: number): number {
    return index + 1
}

const Obj = {
    getData: (index: number): number => {
	return index + 1
    }
}
const Obj = {
    getData: function  (index: number): number {
	return index + 1
    }
}
const Obj = {
    getData (index: number): number {
	return index + 1
    }
}

函数可以设置必传项和默认值

const getData = (str: string): number => {
    return str.length
}
// 如果参数可以不传
const getData = (str?: string): number => { // 会报错 因为 undefined 没有length , 参数加了? 则代表 str为  string 或者 undefined 即为, (str?: string | undefined) => number, 那么为undefined的时候,str是没有length 的,所以会报错。
    // ts考虑了所有的情况,所以就避免出现typeError的情况
    return str.length
}
// 我们可以这么写,给其默认值
const getData = (str?: string): number => {
    str = str || ''  // str在调用length 的时候被推导出肯定为字符串,所以不会报错
    return str.length
}
// 我们也可以这么写
const getData = (str: string = ''): number => { // 入参如果给定了默认值, 那么这个参数将不用使用 "?" 因为这个参数默认就是非必传的
    return str.length
}

5. 交叉类型

交叉类型是将多个类型合并为一个类型。 可以让我们把现有的多种类型叠加到一起成为一种类型,它包含了所需的这几个类型的所有特性,一般用于 interface 的合并, 比如:


interface A {
    name: string
    age: number
}
interface B {
    name: string
    gender: string
}

const a: A & B // a的类型为 {name: string; age: number; gender: string}

交叉类型会把两个interface中的所有项进行合并,相同的项如果类型相同,则该项的类型即为两者共同的类型,比如上述代码中的name, 如果不同,会将两个类型进行合并,比如:

interface A {
    name: string
    age: number
    sayName: (name: string) => void
}
interface B {
    name: string
    gender: string
    sayName: (gender: number) => number
}

const a: A & B // a的类型为 {name: string; age: number; gender: string; sayName: (gender: number) => void && (name: string) => number} // interface中的每一项也会进行合并比如这个示例中的函数

关于函数的交叉类型,代表的是什么作用呢,可能比较难理解,但是我还是说下, 比如

type Fun = (gender: number) => void & (name: number) => number // 如果入参完全相同,则Fun的类型,为接收一个number的参数 返回void, 函数返回值以前者为准
type Fun = (gender: number) => void & (name: string) => number // 如果入参不同, 则Fun的类型为, 参数可以接受数字,如果接收数字,则返回值为 void,就是接收数字的那个类型,反之接收字符串,则返回值为number,&后面的类型。 这个可能在一些特殊场景下用得到

关于基本类型的交叉类型, 比如:

interface A {
    name: string
    age: number
}
interface B {
    name: number
    gender: string
}

const a: A & B // a的类型为 {name: never; age: number; gender: string;} // name 前面是字符串,后面变成了数字,交叉后为什么得到never 呢?  其实 string & number 这种基本类型的交叉,这种类型是不存在的。你想想,有什么数据即是字符串又是数字呢

这里说到never简单提一下,never是完全没有返回值的类型. 你想想一个函数,返回never是什么情形? 不return吗?不是,不return 也是有返回值的,返回值的类型为void, 是null和undefined的父级。 如果返回never就只能有一种情况,就是这个函数在执行的过程中,必然,或者有概率抛出错误,比如:

const fun = (num: number) => {
    if (num > 10) {
	thorw new Error()
    }
    return num + 1
} // 这个函数的返回值,会被推算成  number | never

那么既然有交叉类型,& 肯定就会上述代码出现的联合类型 , 类型1  |  类型2 ,我们看看联合类型

6. 联合类型

我们在开发中,经常会不可避免的遇到一个变量有可能是这个类型 也有可能是那个类型的情况。 比如,后台给你返回了的数据,有时候是字符串,有时候是数字类型的。或者你的函数,需要接收一个数字类型或者字符串类型,然后加工成一个指定类型,比如:

interface DataType {
    id: number | string
}
const Data: DataType = await ajax.get('/api/getData')
// 或者
interface Style { width: string; height: string}
const creatStyle = (size: string | number): Style => {
    let style: Style
    if (isNaN(Number(size))) {
	style= {
            width: size.toString(),
            height: size.toString()
	}
    } else {
	style= {
            width: size + 'px',
            height: size + 'px'
	}
    }
    return style
}

7. 字面量类型

基本数据类型,和 非基本数据类型 的 数组 对象 函数的类型,我们都已经说了。 这些类型可以细分, 但是限制的范围还是非常的宽泛。有些时候,我们可能需要将一个数据限制在很小的范围内,那么我们该怎么做?

interface Info {
    name: string
    id: number
}

interface ShopEcum {
    [x: string]: Info 
}


const getShopInfo = (merchant_num: srting) => {
    const shopEcum: ShopEcum = {
	'20000009': {
            id: 20000009,
             name: 'xxx官方旗舰店'
        }
    }
    return shopEcum[merchant_num]
}
getShopInfo('20000009')  // 上面代码 ts不会报错, 但是你想想,如果我传的不是 20000009呢, 是不是函数会返回 undefined, 返回的类型不一定是Info, 这样就有可能在我们开发的过程中遇到问题
//所以我们精确下类型
interface Info {
    name: string
    id: number
}

type ShopStr = '20000009' // 该类型即为字面量类型,不仅是string 还必须是 这个值

type ShopEcum = {
    [x in ShopStr]: Info; // in为遍历 ShopStr 作为key info作为值,后面会说
}

const getShopInfo = (merchant_num: ShopStr ) => {
    const shopEcum: ShopEcum = {
	'20000009': {
            id: 20000009,
            name: 'xxxxxx官方旗舰店'
       }
   }
    return shopEcum[merchant_num]
}
getShopInfo('20000009') // 如果你传入的是非 20000009 则会报错

那有人就会问,既然这个2000009是写死的,我们要这个类型干啥? 说的没错,如果字面量只有一个值的话,那我们还不如直接写死就行。但是字面量类型可以是多个类型结合,这时候才是字面量类型的意义

iinterface Info {
    name: string
    id: number
}

type ShopStr = '20000009' | '20000000' // 我们在 20000009 上拓展几个店铺。  比如 加个 20000000

type ShopEcum = {
    [x in ShopStr]: Info;
}

const getShopInfo = (merchant_num: ShopStr ) => {
    const shopEcum: ShopEcum = {
	'20000009': {
            id: 20000009,
            name: 'xxx官方旗舰店'
        },
	'20000000': {
            id: 20000000,
            name: 'JD店铺'
        }
    }
    return shopEcum[merchant_num]
}
getShopInfo('20000009') // 如果你传入的是非 20000009 或者非 20000000  则会报错

// 不光可以应用于此, 我们经常会使用后台返回的状态 比如state

const state:number = await ajax.get('/api/getState')  // 虽然我们限制了state的类型,但是number是非常广的, 很有可能在使用中出现越界的情况, 比如state 只有  0  1   2   3 这几种状态,但是你却判断了 state为4的时候。
// 那我们精确下
type StateType = 0 | 1 | 2| 3
const state: StateType = await ajax.get('/api/getState') 

if (state === 4) { // 则会报错,以为state永远都不可能会是4 会提示: 此条件将始终返回 "false",因为类型 "StateType" 和 "4" 没有重叠
    console.log(state)
}

8. 泛型

泛型是对非基本类型的一种拓展。用于复用。 我们如果吧之前所讲的类型理解为值的话,  比如  const val = 值, 那么泛型就代表  const val = (参数)=> 值。  就是说,我们使用一种形参的形式,使用类似函数调用的方式,拿到一个既定的模型的类型。可能说起来,不容易理解,我们看示例:

// 我们请求后台的数据拿到的数据结构一般为这样的
interface ListItem {
    id: number
    name: string
}
interface DataType {
    total: number
    list: Array<ListItem>
}
const Data: DataType = await ajax.get('/api/getData')

// 那我们在调getData2的时候, 又得声明一个 DataType  和 ListItem 
interface ListItem2 {
    id: number
    title: string
}
interface DataType2 {
    total: number
    list: Array<ListItem2>
}
const Data2: DataType2 = await ajax.get('/api/getData2') 
// 我们通过对比就会发现, 后台返回的数据类型 DataType中, 只有 list的项类型不一样,其他的,包括格式都是  totle  和一个数组。 那么我们可以声明一个泛型, 将ListItem作为传参,来生成我想要的 DataType, 这样我们每次调用接口,只需要声明一个 ListItem即可

type ListDataType<T> = {
    totalnum: number
    list: Array<T>
}
const Data: ListDataType<ListItem>= await ajax.get('/api/getData') 
const Data2: ListDataType<ListItem2>= await ajax.get('/api/getData2') 
// 可以看出,我们将ListItem 作为参数,传给ListDataType,然后返回了一个既定的结构; T 即为形参。

那么T即为形参,传任何类型都行,最终都会放在list里面,但是这并不是我们想要的。 我们只想要他放进去一个对象。那我们是不是可以限制其类型呢? 答案是可以的

interface AnyObj = { // 该interface 代表任何一个对象类型
    [x: string]: any
}

type ListDataType<T extends AnyObj> = { // 这时候 T只能传 对象, 如果是非对象,就会报错
    totalnum: number
    list: Array<T>
}
 // T extends AnyObj 代表T必须继承自 AnyObj , 就是说,T必须是AnyObj 的子集或者本身
// 举个其他的例子
type ListDataType<T extends number> = { // 这时候 T只能传 数字类型获取其子集(比如数字的交叉类型), 如果非数字类型,就会报错
    totalnum: number
    list: Array<T>
}
const Data: ListDataType<1 | 2>= await ajax.get('/api/getData') // 正确 因为 1 | 2 是包含在 number重点 所以是number的子集
const Data: ListDataType<number>= await ajax.get('/api/getData') // 正确 number是自己本身
const Data: ListDataType<'2' | 1>= await ajax.get('/api/getData') // 报错  该联合类型 不完全属于 number

那么T即为形参,模仿函数的形参, 就会有非必填和 默认值, 但是需要注意的是,泛型的形参如果非必传, 是必须设置默认值的

// 泛型不能直接拿来用,要传参,但是如果 泛型中的 参数设置了默认值,则可以直接拿来调用


type ListDataType<T = string> = {
    totalnum: number
    list: Array<T>
}

interface ListItem {
    id: number
    title: string
}

const Data: ListDataType<ListItem>= await ajax.get('/api/getData') // list中为 ListItem 
const Data: ListDataType = await ajax.get('/api/getData') // list中为 string

// 注意 interface的泛型,也可以这么写
interface ListDataType<T = string> {
    totalnum: number
    list: Array<T>
}
// 泛型可以接收多个参数
interface ListDataType<T = string, P = number> {
    totalnum: number
    list: Array<T>
    list2: Array<P>
}

上面为对象的泛型, 其实数组和元组的泛型也差不多

type ListType<T> = Array<{
    id: number
    data: T
}>

type TupleType<T> = [T, T, T]

函数的泛型,函数的泛型和上面的有所不同,主要体现在,泛型参数默认非必传。 我们看下使用场景

// 我们将函数类型区分为,函数类型声明 和 匿名函数设置类型 即为等号左边的和右边的

type FunType  = <T>(val: T) => { isEmpty: boolean; data: T }  // 该函数类型是, 我们传入一个任意的值, 最终返回生成一个对象,对象中包含一个 isEmpty 的属性, 和data , data即为传入的值。 
// 我们通过这个可以看出, 这个函数并不关注参数的类型,只是将其加工后再次返回

// 下面是匿名函数的声明的时候泛型设置方式
const fun: FunType = <T>(val: T): { isEmpty: boolean; data: T } => {
    return {
        isEmpty: !!val,
        data: val
    }
}
// 在tsx中 要这么写才不会报错
const fun: FunType = <T extends any>(val: T): { isEmpty: boolean; data: T } => {
    return {
        isEmpty: !!val,
        data: val
    }
}

// 我们在调用该函数的时候,可以不传泛型的参数

fun({a: 1}) // 不传参数ts会根据参数推导出来的类型,作为T的值, 比如该函数的形参为 val, 你传入的 值为 {a: 1}, 那么T的类型值为 {a: number}

// 有些情况我们需要传参数类型 比如
const data = ajax.get('/api/getData') // data 默认为any
fun<{a : number}>(data) // 该函数中的泛型形参T 即为 {a: number}, 而不是 val的推导类型any 了。 该操作的直接结果为,函数调用后返回值的类型发生变化

// 说一个简单的使用场景
// 比如我们需要拷贝一个对象
const clone = <T extends AnyObj | Array<any>>(obj: T): T => {
    return JSON.parse(JSON.stringify(obj))
}


我们看完泛型函数之后,就会明白,泛型类其实也是异曲同工


// 我们声明一个 DataCache ,这个类的作用就是,储存放入的内容,然后可以通过索引值获取
class DataCache<T> {
    arr: Array<T> = []
    set (data: T) {
	this.arr.push(data)
    }
    get (index: number): T {
	return this.arr[index]
    }
}

const obj = new DataCache()  // 我们在new 的时候并没有传入泛型的参数,所以 T会默认为 unknown , 也就是你会看到  obj.get 的类型为  (index: number)=> unkonwn

// 我们改一下 加一个 constructor
class DataCache<T> {
    constructor (defData: T) {
	this.set(defData)
    }
    arr: Array<T> = []
    set (data: T) {
	this.arr.push(data)
    }
    get (index: number): T {
	return this.arr[index]
    }
}

const obj = new DataCache(2)  // 我们在new 的时候并没有传入泛型的参数,但是传入构造参数,所以泛型类会根据构造参数的类型,推导出T的类型 , 也就是你会看到  obj.get 的类型为  (index: number)=> number

// 当然我们也可以传入参数
const obj = new DataCache<number>() // obj.get 的类型为  (index: number)=> number

// 泛型类的继承也是一样的
class DataCache<T> {
    arr: Array<T> = []
    set (data: T) {
	this.arr.push(data)
    }
    get (index: number): T {
	return this.arr[index]
    }
}

class Cus extends DataCache<number> {
	getDouble (index: number) {
		return [ this.get(index), this.get(index + 1) ]
	}
}
const a = new Cus()
a.getDouble(1) // a.getDouble的类型为  (index: number)=> [ number, number ]

// 泛型类也可以继承泛型类, 并将T 传给被继承的类
class Cus<T> extends DataCache<T> {
    getDouble (index: number): [ T, T ] {
        return [ this.get(index), this.get(index + 1) ]
    }
}
const a = new Cus<number>()
a.getDouble(1) //  a.getDouble的类型为  (index: number)=> [ number, number ]


二、类型的使用

我们声明了类型后,需要在开发过程中使用。所以类型之间的转换、计算以及使用也是我们必须得掌握的东西。所以下面就给大家讲讲, 上面我们声明的这些类型的常见使用场景。

1. 类型的计算——联合类型 和 interface 的互相转换

有时候,我们的联合类型会和 对象的类型 interface进行相互的转换。典型的例子就是 用key访问 一个对象的属性。我们如果不限制key的值, 那么key会出现任何值,导致访问的属性为空。 那我们如果想要限制访问的key的话,我们还需声明一个联合类, 和这个对象的interface的keys相同,这样很麻烦,所以我们用到了一个关键字  keyof


// 我们继续用上面的示例做例子
interface Info {
    name: string
    id: number
}

interface ShopEcum {
    '20000009': Info 
    '20000000': Info 
}

// type ShopStr = '20000009' | '20000000' // 我们写的这个联合类型和上面的 ShopEcum keys完全一样,那么为啥要写2遍呢
// 可以这么写
type ShopStr = keyof ShopEcum // keyof 的意思就是取一个 interface中的key 组成一个联合烈性。 有点像js重点 Object.keys() 方法

const getShopInfo = (merchant_num: ShopStr ) => {
    const shopEcum: ShopEcum = {
	'20000009': {
            id: 20000009,
            name: 'xxx官方旗舰店'
        },
	'20000000': {
            id: 20000000,
            name: 'xx店铺'
        }
    }
    return shopEcum[merchant_num]
}
getShopInfo('20000009')

除此之外,我们也可以用 关键字 “in”  来讲联合类型转为interface


// 我们继续用上面的示例做例子
interface Info {
    name: string
    id: number
}

interface ShopEcum {
    '20000009': Info 
    '20000000': Info 
}

type ShopStr = '20000009' | '20000000'

type ShopEcum =  {
    [x in ShopStr]: Info; // in 关键字会遍历 ShopStr 让ShopStr 的每一项作为key值都为 Info
}
// 记住,如果我们使用in,那么interface声明不能用 interface关键字,否则会报错,比如这样
interface ShopEcum  { // 这么写是错误的
    [x in ShopStr]: Info; // in 关键字会遍历 ShopStr 让ShopStr 的每一项作为key值都为 Info
}

const getShopInfo = (merchant_num: ShopStr ) => {
    const shopEcum: ShopEcum = {
	'20000009': {
            id: 20000009,
            name: 'xxx官方旗舰店'
        },
	'20000000': {
            id: 20000000,
            name: 'xx店铺'
        }
    }
    return shopEcum[merchant_num]
}
getShopInfo('20000009')

2. interface的继承

我们经常会用到一个interface 中会包含另一个interface的情况,但是这两个都有单独使用的场景, 那我们该怎么去声明这两个interface呢?

interface GiftType {
    id: number
    name: string
}

interface GiftTypeAndState {
    id: number
    name: string
    isUse: boolean
}

const isUse = [1,2,3]
const gift: Array<GiftType> = await ajax.get('/api/getGift')

const giftState: GiftTypeAndState  = gift.map((gift: GiftType) => ({
    id: gift.id,
    name: gift.name,
    isUse: isUse.includes(gift.id)
}))

// 我们会发现, GiftTypeAndState 会包含 GiftType, 那我们声明GiftTypeAndState 的时候,可以使用继承,也就是extends
// 有点像交叉类型, 但是继承的区别在于,如果当前继承的interface中有和被继承的interface有相同项, 交叉类型会让该类型进行交叉,而继承会用当前的相同的属性类型代替被继承的属性类型
interface GiftTypeAndState extends GiftType { 
    isUse: boolean
}
// 比如 如果
interface GiftTypeAndState extends GiftType { // GiftTypeAndState 的id的类型为 string, 不再是 number, 但是 交叉类型会 id为  string | number  也就是 never
    id: string
    isUse: boolean
}
// interface继承多个的时候这么写
// 用 “,” 隔开需要依次继承的
interface GiftTypeAndState extends GiftType, GiftType2 {
    id: string
    isUse: boolean
}

3. 类型断言

我们在日常的开发中, 可能会遇到一些情况,导致你将一个数据赋值给一个变量,但是这个数据和要赋值的变量并不一致的情况,这个时候ts就会报错,说不能将XXX 赋值给XXX  XXX缺少XXX中的XX属性之类的。 那遇到这种情况我们该如何处理呢?

// 首先,我们将不属于这个类型的值赋值给这个类型的变量,原则上是有问题的。 但是,在某些时候,我们不得不这么做的时候,ts也给我们开通了一个通道, 那就是 类型断言

interface DataType {
    id: number
}
const data : DataType = {} // 会报错
// 我们可以将 等号右边的值, 断言为 DataType , 这个时候,ts就会认为,{} 的类型就是 DataType , 虽然实际上不是。
const data: DataType = <DataType>{}
// 或者
const data: DataType = {} as DataType   // 注意 tsx中只能有as 这种方式 因为 尖括号形式会被误认为是标签

// 还有一种情况
const data: DataType = <DataType>{ name: '小王' } // 这种也会报错, 如果你写成下面的形式
const data: DataType = { name: '小王' } as DataType  // 这样依然会报错, 因为 name属性 和 id 属性无法重叠,所以这样的写法是错的, 那我们如果非要这么做怎么办?

const data: DataType = { name: '小王' } as unknown as DataType 
// 或者
const data: DataType = <DataType><unknown>{ name: '小王' }

代码中过多的断言是非常不优雅的,并且很有可能影响代码的逻辑和准确性,所以我一般不推荐大家这么做。 但是在有些场景下,我们可能为了实现业务,就必须用到了, 比如以下场景:


// 场景1
// 我们初始化声明一个变量data,去接受一个对象,这个对象是一大堆数据, 你声明了interface,  data在一开始就会通过某种方法获取到实际的值,但是如果在初始化的时候,就填写默认值,那么会非常的麻烦, 那我们可以使用断言的方式
interface DataType {
    val1: striing
    val2: number
    ...
    valn: string
}
const data: DataType = {} as DataType

if (xxx) { // xxx在当前条件下必然为true, 但是ts 的代码检测却无法推导出其为true 的情况。  否则我们可以直接声明 const data: DataType 不给其赋值。
    data = await ajax.get('getData') // ajax 返回的数据即为 DataType 类型
}

use(data)
// 其实上面的我还是不建议大家使用的,因为在某些情况下, 必然react或者vue, 数据初始化的时候,是会被渲染一次的, ajax是个异步的,更新后又会被渲染。这样就会导致一个问题,就是在初始化渲染的时候,数据结构不正确导致报错

// 场景2
// 第三方工具声明的方法,和实际的数据不符 。在taro 中我就遇到过。  
// taro主要是针对微信的, 所以类型上面声明中,并没有对 淘宝小程序中的特定方法声明类型。 导致 缺少一些属性或者方法。 比如 canvas 组件
import { ComponentType } from 'react'
import { Canvas } from '@tarojs/components'
import { CanvasProps } from '@tarojs/components/types/Canvas'
// 阿里小程序中 canvas 组件必须接收一个 width 和height 属性, 但是taro Canvas组件中却没有该属性的声明 如果你强加就会报错

<canvas width={100} height={100} /> // ts会报错  但是实际上 阿里的canvas是有该属性的

// 那么我们 就需要给 CanvasProps中添加 这两个属性
interface MyCanvasProps extends CanvasProps { // 由于CanvasProps 并不是全局属性,所以我们不能使用 接口融合的方式 去修改CanvasProps , 只能使用一个新的 interface去继承 CanvasProps 
  width: number
  height: number
}
//  然后我们创建一个新的变量 接收canvas
const MyCanvas: ComponentType<MyCanvasProps> = Canvas as unknown as ComponentType<MyCanvasProp>

<MyCanvaswidth={100} height={100} /> // 这样就ok了

还是不建议大家用断言,因为断言可以偷懒,但代价就是有可能会打破代码的稳定性。 除非万不得已, 尽量别用 any 和  断言

4. 安全链式调用 & 强制链式调用

我们在调用对象的属性的时候。经常会遇到链式调用, 比如  a.b.c.d() 如果这样调用,我们就必须要保证 在调用d的时候。a、b、c都是存在的,否则我们就需要进行判断,比如 a && a.b && a.b.c, 但是在ts中,我们可以使用安全链式调用的方法


// 普通js
a && a.b && a.b.c && a.b.c()

// ts中
a?.b?.c()  // 会被编译成上述代码  其返回值也和上述代码一样

// 需要注意的是,如果我们需要获取c的返回值, 如果使用链式调用,c的返回值将为   undefined | 预期的值, 因为,如果父级没有c甚至没有b,这个表达式的返回值为 undefined

const a = {
    set: () => { console.log(2); return 3333}
}

const b: { a?: {set: () => number}} = {
    a
}
const x: number = b.a?.set()  // 会报错, 因为b.a?.set()的返回值,不是 set函数的预期返回值 number, 而是 undefined | number 


// 强制链式调用和 安全链式调用有所不同, 前者会涉及到运行时, 后者只是编译是
// 如果我们在开发的时候,能够保证,在某个条件下,一个对象里面的一个属性是必然存在的,但是其类型却是可选值。我们可以使用强制链式调用的方法,让其ts不报错。(前提是我们得保证,这里调用的时候,却是是有这个值。 有点像 断言)
const a = {
    set: () => { console.log(2); return 3333}
}

const b: { a?: {set: () => number}} = {
    a
}
const x: number = b.a!.set() // 这里不会报错, 因为ts认为这个代码肯定会调用set方法

// 如果a 不存在的话,
const a = undefined
const b: { a?: {set: () => number}} = {
    a
}
b.a!.set() // 编译时不会报错, 但是运行时就会报错

5. 类型声明文件

类型声明我们可以在执行的代码中进行声明,也可以放在声明文件里面。 声明文件有两种,一种是全局声明文件(在该文件中声明的类型, 不需要导入,可以直接使用), 模块声明文件(需要导入才能使用)

全局声明文件:


// 放在任意ts includes范围里面的ts文件
declare interface AnyObj{
  [x: string]: any
}


declare type state = 0 | 1 | 2;

局部声明文件:


// 放在任意ts includes范围里面的ts文件
import { DataType } from "Typings"
export interface AnyObj{
    [x: string]: DataType 
}

export type state = 0 | 1 | 2;

我们会发现,但凡有inport或者export的都为局部声明文件,需要导入才可以使用。 其中能访问全局类型,但是全局类型声明文件中,不可以直接使用局部的。 这用于防止类型污染,相当于我们给指定功能创建了自己独立的类型声明文件

6. 命名空间

命名空间主要用于我们将一些类型进行分类。比如,我们将很多组件或者方法放在了一个工程中,业务工程引入了该工程,我们在使用这个库的时候,有时候会用到库所对应功能的 类型, 比如函数的入参类型, 组件的props 类型。 那么我们需要将方法,或者组件导出的同时也将 类型导出。 但是如果吧这些类型放在一起导出,会倒是很混乱,所以我们使用 namespace 将其分类

// 方法1 fun1.ts
import { Fun1ArgType, Fun1ResType } from './type.d.ts';
export default (arg: Fun1ArgType): Fun1ResType => {
    const res: Fun1ResType = doSomesting()
    return res
}

// 方法2 fun2.ts
import { Fun2ArgType, Fun2ResType } from './type.d.ts';
export default (arg: Fun2ArgType): Fun2ResType  => {
    const res: Fun2ResType = doSomesting()
    return res
}

// 类型声明文件 type.d.ts
export interface Fun1ArgType { xxx }
export interface Fun1ResType{ xxx }
export interface Fun2ArgType{ xxx }
export interface Fun2ResType { xxx }

// 导出出口
import fun1 from './fun1.ts';
import fun2 from './fun2.ts';

import { Fun1ArgType, Fun1ResType, Fun2ArgType, Fun2ResType } from './type.d.ts';

export {
    fun1,
    fun2,
    Fun1ArgType,
    Fun1ResType,
    Fun2ArgType,
    Fun2ResType
}
// 你会发现,我们除了用 type 的名字去区别, 没有对类型进行更好的归类,在使用的时候,我们很难清楚的知道该类型到底属于那个函数

这时候我们就可以使用 命名空间来解决这个问题了,来看代码:


// 方法1 fun1.ts
import { Fun1ArgType, Fun1ResType } from './type.d.ts';
export default (arg: Fun1ArgType): Fun1ResType => {
    const res: Fun1ResType = doSomesting()
    return res
}

// 方法2 fun2.ts
import { Fun2ArgType, Fun2ResType } from './type.d.ts';
export default (arg: Fun2ArgType): Fun2ResType  => {
    const res: Fun2ResType = doSomesting()
    return res
}

// 类型声明文件 type.d.ts
export namespace Fun1Types { 
    export interface ArgType{
        a: string
        b: number
    }
    export type ResType = 1 | 2 | 3
}
export namespace Fun2Types { 
    export interface ArgType{
   	name: string
    	id: number
    }
    export type ResType = Array<Fun2Types.ArgType> // 可以递归调用
}

// 导出出口
import fun1 from './fun1.ts';
import fun2 from './fun2.ts';

import { Fun1Types, Fun2Types } from './type.d.ts';

export {
    fun1,
    fun2,
    Fun1Types,
    Fun2Types
}
// 我们在调用方,可以导入工程,拿到Fun1Types,去使用 , 比如

import { fun1 , Fun1Types } from 'toos';

const res: Fun1Types.ResType = fun1({a: '', b: 1}) // 其实fun1函数中声明了 返回值类型,我们不一定必须给res声明类型,ts会根据fun1的返回值自动推导出res 的类型,但是这里写上的原因是,为了让自己清楚当前值的类型

7. 其他

ts还有很多更深层次的用法,但是在我们的业务中,几乎很难用到。 这里列举几个可能会用到的例子,以便不时之需

我们需要声明一个类型来,限制一个对象,这个对象必须有个固定的属性,其他属性值可有可无,可以是任何值

// 我们声明一个interface 里面必须有个id,其他值随便
interface RequiredObj {
    id: number
    [x: string]: any
}

const A: RequiredObj = { id: 1 } //正确
const A: RequiredObj = { id: 1, a: 2 } // 正确
const A: RequiredObj = { a: 2 } // 错误


我们需要声明一个类型,这个类型限制一个对象,必须是另一个对象的子集, 包括自身


type Partial<T> = {
    [P in keyof T]?: T[P]
}

interface DataType {
    id: number
    name: string
    age: number
}

const A: Partial<DataType> = { // 正确 因为赋值的对象只有一个 id 并且都为 DataType的子集
    id: 1
}


const A: Partial<DataType> = { // 错误, 因为DataType 的子集中不包含 x属性
    id: 1,
    name:'',
    x: 3
}
// 这种应用场景为  我们写一个函数和一个固定的对象,这个函数也接收一个对象, 入参的对象值,必须为这个对象的子集, 最后用传入的对象,并入固定的对象,并返回修改后的值
const Obj: DataType = { id: 1, name: '小王', age: 18 }
const upDateObj = (argObj: Partial<DataType>): DataType  => {
    return {
        ...Obj,
        ...argObj
    }
}
upDateObj({id: 2}) // 正确
upDateObj({id: 2, x: 3}) //错误
// 其实Partial这个泛型 ts本身就已经存在了,这里写出来只是说明下其原理

我们经常会使用到Promise, 那么针对 async修饰的函数,我们如何设定其返回值呢?


interface DataType {
    id: number
    name: string
    age: number
}


const getData = async (id: number): Promise<DataType> => { // 针对 async 修饰的函数,返回的值必然是Promise类型,所以如果需要设置其返回值的类型, 需要使用Promise泛型, 泛型参数即为 该函数中返回的Promise的resolve函数调用时候,传入的值的类型
    return await ajax.get('/api/getData', { id })
}
// 比如
const getData = async (id: number): Promise<DataType> => {
    return new Promise((resolve: (res: DataType) => void) => {
        resolve({
            id,
            name: '小王',
            age: 18
        })
    })
}
// 比如
const getData = async (id: number): Promise<DataType> => { // 虽然我们在函数中直接return 一个对象, 但是因为async 修饰, 所以该函数的返回值 依然是promise类型
    return {
        id,
        name: '小王',
        age: 18
    }
}

关于class 以及其类型声明

// 对于class 来说,有个特殊的地方就是, 如果创建了一个 class  那么 会自动生成一个 和 当前Class 同名的 interface

// 比如
class NumData {
    constructor (x: number) {
        this.x = x
    }
    x: number
    getData (y: number) {
        return this.x + y
    }
}
// 除了有一个 NumData 的class 可以用于你new 之外, 还会自动生成一个 名叫 NumData 的interface, 该interface 会自动赋值于 new NumData(10) 的结果
const numData = new NumData(10) // numData 的类型推导为 NumData 

// 同时可以将 NumData 指定给任何符合该类型的对象, 比如

const Obj: NumData = { // 虽然getData 的函数体不一样,但是其中x的类型 和getData方法的类型是完全和 NumData 中都一样 的
    x:1,
    getData: (y: number) => {
        return y++
    }
}


最后, 所有的类型声明尽量有大驼峰的形式, 首字母大写会很轻易的区分出类型和 变量。 否则,将其混淆,也是很麻烦的一件事。这些规范,在eslint中也会有的。

基本上能用到的也就这些了, 如果大家有什么补充的,可以在评论里面补充。

作者:徐晨