ts高级类型与鲜为人知的语法

741 阅读8分钟

在第一次使用ts的时候,是自己做的一个小东西。那时候ts使用的很少(至少在我的身边)。然而现在学习 TypeScript 的小伙伴越来越多了。现在越来越多的公司使用ts,已经成为了一种标配。同时发现自己的ts水平实在欠佳,经过一段时间的学习,做出总结,如果不对的地方,欢迎指出。

起始

TypeScriptJavaScript
JavaScript 的超集用于解决大型项目的代码复杂性一种脚本语言,用于创建动态网页
可以在编译期间发现并纠正错误作为一种解释型语言,只能在运行时发现错误
强类型,支持静态和动态类型弱类型,没有静态类型选项
最终被编译成 JavaScript 代码,使浏览器可以理解可以直接在浏览器中使用
支持模块、泛型和接口不支持模块,泛型或接口

最近的一些学习,彻底搞懂了ts的高级类型,和一些自己独有的语法。在项目中,你肯定遇到过一个接口有很多属性,必填非必填有很多。这时候,你想某一项或者几项变成非必填或者必填就很麻烦。在ts的高级类型中没有一个可以做到这件事情的类型,可能会重写这样的一个新的接口。不过当彻底搞懂ts高级类型之后,就可以自己写出来这样的一个高级类型;

type PortionPartial<T, K extends keyof T> = Omit<T, K> & {
    [in K]?: T[P]
};
type RequiredPartial<T, K extends keyof T> = Omit<T, K> & {
    [in K]-?: T[P]
};

PortionPartial可以将T接口中的k属性变为可选,可以一次传入多个属性。 RequiredPartial可以将T接口中的k属性变为必选,可以一次传入多个属性。

这就解决了之前说过的问题,可以在编辑器中尝试使用,可以达到你想要的效果。如果你的ts功底不好,那可能看起来有点吃力。但是当你看完这篇文章,就可以轻松自己写出想要的高级类型。

正文

ts有很多高级类型,比如Omit<T, K extends keyof any>可以将T接口中的K属性剔除,返回一个新的类型。Pick<T, K extends keyof T>可以将T接口中的T属性拿出返回一个新的类型......

ts的所有的高级类型都离不开泛型的支持,可以在编辑器中看到,所有的高级类型都有泛型的身影。

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候指定类型的一种特性。

泛型是一个很重要的知识概念,这篇文章主要讲解ts的高级类型,所以泛型不会的同学可以自行搜索,网上对于泛型的文章还是很多的。

语法in extends infer...

在高级类型的解析之前,首先有一些ts的语法需要了解,比如in、extends、infer等等,这些东西在平常项目中可能用处比较少,但是在一些源码库和ts高级类型中却是必不可少的。

typeof

const people = {
    name: "张三",
    age: 33
}

type People = typeof people;
//People = {name: string;age: number;}

将一个属性转换为和这个属性相对应的类型。

keyof

type KeyofType = keyof People;
//  KeyofType = "name" | "age"

keyof可以将一个接口中的属性名全部取出组成一个具体的字符串对象。

在使用keyof时,any是一个特殊的存在。any被keyof解释之后是string | number | symbol

type OneType<T> = T extends keyof any ? T : never;
// keyof any === type OneType = string | number | symbol

type OneResultType = OneType<boolean>
// never

extends

在泛型中extends不再是继承的意义,而是一种约束。

如果 T 不是一个联合类型,表示如果 T 是 U 的子集,那么返回 X 否则返回 Y。

export type TExtends<T, U> = T extends U ? number : never;

// T(number)是 U(number | string)的子集,所以返回number
type TExtendsExample1 = TExtends<numbernumber | string>; // number

// T(boolean) 不是 U(number | string)的子集,所以返回never
type TExtendsExample2 = TExtends<booleannumber | string>; // never

如果 T 是一个联合类型,表示如果 T 中的类型是 U 的子集,那么返回 X 否则返回 Y。这个过程可以理解为对T中的类型进行一次遍历,每个类型都执行一次 extends。

type NonNullable<T> = T extends null | undefined ? never : T;

// T(number | string) 不是 U(null | undefined) 的子集,所以返回 T
type TNonNullableExample1 = NonNullable<number | string>; // number | string

// T(string | null) 中 string 不是 U 的子集返回 string,null 是 U 的子集,返回 never
type TNonNullableExample2 = NonNullable<string | null>; // string

infer

infer 推导类型 通常与extends配合使用出现在三种地方

1 出现在extends条件语句后的函数类型的参数类型位置上

2 出现在extends条件语句后的函数类型的返回值类型位置上

3 出现在类型的泛型的具体化类型上

type fn = (param: number) => string
type fn2 = (param: number, param2: string) => string
type InferType<T> = T extends (param: infer P) => unknown ? P : T
// 在类型InferType中会判断传入的类型是不是接受一个属性,返回一个值得函数,如果成立则返回这个函数属性的类型,否则返回传入的T类型
// 传入fn:因为与定义的(param: infer P) => unknown类型匹配,则p代表推导出的fn参数的类型,进行返回则得到number类型
// 传入fn2:因为fn2需要的参数比InferType中需要的参数更多则约束不成立,得到传入的fn2类型
//(在extends中 函数的情况下 如果前置的函数类型参数比后置的函数类型参数少,其他条件相同的情况下成立)

type InferTypeTwo<T> = T extends (paramnumber) => infer P ? P : T
// 传入fn:类型匹配成立,则p代表推导出的fn的返回值的类型,进行返回则得到string类型
// 传入fn2:类型匹配不成立,得到传入的fn2类型

在类型InferType中会判断传入的类型是不是接受一个属性,返回一个值的函数,如果成立则返回这个函数属性的类型,否则返回传入的T类型

in

在泛型中可以遍历一个联合属性或者接口,遍历接口时会将接口中的值全部拿出。

type CreateInterfaceType<K extends keyof any, T> = {
    [P in K]: T;
};

type ObjType = CreateInterfaceType<"name" | "age", boolean>;
// {name: boolean;age: boolean;}

高级类型

Extract

type Extract<T, U> = T extends U ? T : never;

type Type1 = Extract<stringobject>
// never
type Type2 = Extract<"string" | "number" | "name" | "age""number" | "string" | "boolean" | "name">
// "string" | "number" | "name"
type Type3 = Extracts<{usernamestringusername2string}, {usernamestringusername2stringusername3string}>
// never

将传入的T类型中在U类型同样存在的属性取出,在没有匹配项时返回never。

Exclude

作为和Extract相反的存在

type Exclude<T, U> = T extends U ? never : T;
type Type4 = Exclude<"string" | "number" | "name" | "age""number" | "string" | "boolean">
// "name" | "age"

将传入的T类型中在U类型不存在的属性取出,若全部匹配则返回never。

Record

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

K必须是string | number | symbol中的一个,返回一个接口,属性名将K遍历得到,T作为属性值出现。 在之前肯定有人和我一样定义一个obj的时候

intefeace Obj {
  [keystring]: string
}

以后就可以直接写Record<string, string>达到一样的结果。

在定义式Record<number, string>可以完全匹配给Record<string, string>,反之不成立。

Partial

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

将传入的泛型接口中所有属性值定义为非必填。

Required

type Required<T> = {
    [P in keyof T]-?: T[P];
};

将传入的泛型接口中所有的属性定义为必填。

这里用的是-?去掉非必填的?,并非重新定义为[P in keyof T]-?: T[P]。否则会失去效果。

例如

type RequiredTest<T> = {
    [P in keyof T]: T[P];
};
// 这样返回的类型会毫无改变,因为比如{username?: string}在非必填属性取到的属性是string | undefined;
type TestA = RequiredTest<{name: string, age?: number}>
// 得到的类型并非我们想要的,而是{name: string;age?: number | undefined;}

ReadOnly

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

type TestA = Readonly<{name: string}>

const testA: TestA = {
    name: "name",
}
testA.name = "asdf"// 报错:无法分配到 "name" ,因为它是只读属性。

将属性变为只读从而无法修改。其实在属性初始化时,可以将其使用强转符as强转为const也可以达到一样的效果

const testB = {
    name: "name"
} as const;

testB.name = "name2"// 报错:无法分配到 "name" ,因为它是只读属性。

pick

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

将K遍历a构成一个新的接口,将T中对应的属性拿出。作为复杂的高级类型Omit的一个成员

Omit

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
interface Test {
    name?: string;
    age?: string;
    six?: string
}

type OmitTest = Omit<Test"name">
//{age?: string;six?: string;}

omit作为一个比较复杂高级类型,Exclude的功能是将第一个联合类型遍历,剔除第二个属性上同时存在的联合类型,得到了"age"|"six",之后使用pick遍历得到的联合类型,将T接口中存在属性取出返回新的接口。

结束

ts的语法与高级类型远不止文章中提高的这一步,到这里的时候,剩下的高级类型解读已经没有任何压力。ts作为一门静态语言,很好的在代码的编译过程帮你找出粗心导致的问题。相信未来ts会被越来越多的应用,vue3源码已经全面使用,在ts的功底之上,阅读源码会方便很多。

最后!

再放一次文章开始的两个自己写的高级类型,有用!!!

type PortionPartial<T, K extends keyof T> = Omit<T, K> & {
    [in K]?: T[P]
};
type RequiredPartial<T, K extends keyof T> = Omit<T, K> & {
    [in K]-?: T[P]
};