function foo(arg: T): T {
return arg;
}
我们使用T
来表示一个未知的类型,它是入参与返回值的类型,在使用时我们可以显示指定泛型:
通常泛型只会使用单个字母。如T U K V S等。我的推荐做法是在项目达到一定复杂度后,使用有具体含义的泛型,如
BasicSchema
。
foo(“linbudu”);
const [count, setCount] = useState(1);
当然也可以不指定,因为 TS 会自动推导出泛型的实际类型。
泛型在箭头函数下的书写:
const foo = (arg: T) => arg;
如果你在 TSX 文件中这么写,
<T>
可能会被识别为 JSX 标签,因此需要显式告知编译器:
const foo = <T extends {}>(arg: T) => arg;
除了用在函数中,泛型也可以在类中使用:
class Foo<T, U> {
constructor(public arg1: T, public arg2: U) {}
public method(): T {
return this.arg1;
}
}
泛型除了单独使用,也经常与其他类型编程语法结合使用,可以说泛型就是 TS 类型编程最重要的基石。单独对于泛型的介绍就到这里(因为单纯的讲泛型实在没有什么好讲的),在接下来我们会讲解更多泛型的高级使用技巧。
索引类型与映射类型
在阅读这一部分前,你需要做好思维转变的准备,需要认识到 类型编程实际也是编程。就像你写业务代码的时候常常会遍历一个对象,而在类型编程中我们也会经常遍历一个接口。因此,你可以将一部分编程思路复用过来。我们实现一个简单的函数:
// 假设key是obj键名
function pickSingleValue(obj, key) {
return obj[key];
}
要为其进行类型定义的话,有哪些需要定义的地方?
-
参数
obj
-
参数
key
-
返回值
这三样之间是否存在关联?
-
key
必然是obj
中的键值名之一,一定为string
类型 -
返回的值一定是obj 中的键值
因此我们初步得到这样的结果:
function pickSingleValue(obj: T, key: keyof T) {
return obj[key];
}
keyof
是 索引类型查询的语法, 它会返回后面跟着的类型参数的键值组成的字面量类型(literal types
),举个例子:
interface foo {
a: number;
b: string;
}
type A = keyof foo; // “a” | “b”
是不是就像Object.keys()
?
字面量类型是对类型的进一步限制,比如你的状态码只可能是 0/1/2,那么你就可以写成
status: 0 | 1 | 2
的形式。
字面量类型包括字符串字面量、数字字面量、布尔值字面量。
这一类细碎的基础知识会被穿插在文中各个部分进行讲解,以此避免单独讲解时缺少特定场景让相关概念显得过于单调。
还少了返回值,如果你此前没有接触过此类语法,应该会卡住,我们先联想下for...in
语法,遍历对象时我们可能会这么写:
const fooObj = { a: 1, b: “1” };
for (const key in fooObj) {
console.log(key);
console.log(fooObj[key]);
}
和上面的写法一样,我们拿到了 key,就能拿到对应的 value,那么 value 的类型也就不在话下了:
function pickSingleValue(obj: T, key: keyof T): T[keyof T] {
return obj[key];
}
这一部分可能不好一步到位理解,解释下:
interface T {
a: number;
b: string;
}
type TKeys = keyof T; // “a” | “b”
type PropAType = T[“a”]; // number
你用键名可以取出对象上的键值,自然也就可以取出接口上的键值(也就是类型)啦~
但这种写法很明显有可以改进的地方:keyof
出现了两次,以及泛型 T 应该被限制为对象类型,就像我们平时会做的那样:用一个变量把多处出现的存起来,在类型编程里,泛型就是变量。
function pickSingleValue<T extends object, U extends keyof T>(
obj: T,
key: U
): T[U] {
return obj[key];
}
这里又出现了新东西extends
… 它是啥?你可以暂时把T extends object
理解为T 被限制为对象类型,U extends keyof T
理解为泛型 U 必然是泛型 T 的键名组成的联合类型(以字面量类型的形式,比如T的键包括a b c,那么U的取值只能是"a" “b” "c"之一)。具体的知识我们会在下一节条件类型讲到。
假设现在不只要取出一个值了,我们要取出一系列值,即参数2将是一个数组,成员均为参数1的键名组成:
function pick<T extends object, U extends keyof T>(obj: T, keys: U[]): T[U][] {
return keys.map((key) => obj[key]);
}
// pick(obj, [‘a’, ‘b’])
有两个重要变化:
-
keys: U[]
我们知道 U 是 T 的键名组成的联合类型,那么要表示一个内部元素均是 T 键名的数组,就可以使用这种方式,具体的原理请参见下文的 分布式条件类型 章节。 -
T[U][]
它的原理实际上和上面一条相同,首先是T[U]
,代表参数1的键值(就像Object[Key]),之所以单独拿出来是因为我认为它是一个很好地例子,表现了 TS 类型编程的组合性,你不感觉这种写法就像搭积木一样吗?
索引签名 Index Signature
索引签名用于快速建立一个内部字段类型相同的接口,如
interface Foo {
}
那么接口 Foo 就被认定为字段全部为 string 类型。
等同于
Record<string, string>
值得注意的是,由于 JS 可以同时通过数字与字符串访问对象属性,因此keyof Foo
的结果会是string | number
。
const o: Foo = {
1: “芜湖!”,
};
o[1] === o[“1”]; // true
但是一旦某个接口的索引签名类型为number
,那么使用它的对象就不能再通过字符串索引访问,如o['1']
,将会抛出Element implicitly has an 'any' type because index expression is not of type 'number'
错误。
映射类型 Mapped Types
映射类型同样是类型编程的重要底层组成,通常用于在旧有类型的基础上进行改造,包括接口包含字段、字段的类型、修饰符(只读readonly 与 可选?
)等等。
从一个简单场景入手:
interface A {
a: boolean;
b: string;
c: number;
d: () => void;
}
现在我们有个需求,实现一个接口,它的字段与接口 A 完全相同,但是其中的类型全部为 string,你会怎么做?直接重新声明一个然后手写吗?这样就很离谱了,我们可是机智的程序员。
如果把接口换成对象再想想,假设要拷贝一个对象(假设没有嵌套),new 一个新的空对象,然后遍历原先对象的键值对来填充新对象。再回到接口,其实也一样:
type StringifyA = {
};
是不是很熟悉?重要的就是这个in
操作符,你完全可以把它理解为for...in
/for...of
这种遍历的思路,获取到键名之后,键值就简单了!
type Clone = {
};
掌握这种思路,其实你已经接触到一些工具类型的底层实现了:
你可以把工具类型理解为你平时放在 utils 文件夹下的公共函数,提供了对公用逻辑(在这里则是类型编程逻辑)的封装,比如上面的两个类型接口就是~
先写个最常用的Partial
尝尝鲜,工具类型的详细介绍我们会在专门的章节展开:
// 将接口下的字段全部变为可选的
type Partial = {
K in keyof T?: T[k];
};
是不是特别简单,让你已经脱口而出“就这!”,类似的,还可以实现个Readonly
,把接口下的字段全部变为只读的。
条件类型 Conditional Types
条件类型的语法实际上就是三元表达式,看一个最简单的例子:
T extends U ? X : Y
如果你觉得这里的 extends 不太好理解,可以暂时简单理解为 U 中的属性在 T 中都有。
为什么会有条件类型?可以看到通常条件类型通常是和泛型一同使用的,联想到泛型的使用场景,我想你应该明白了些什么。对于类型无法即时确定的场景,使用条件类型来在运行时动态的确定最终的类型(运行时可能不太准确,或者可以理解为,你提供的函数被他人使用时,根据他人使用时传入的参数来动态确定需要被满足的类型约束)。
条件类型理解起来更直观,唯一需要有一定理解成本的就是 何时条件类型系统会收集到足够的信息来确定类型,也就是说,条件类型有时不会立刻完成判断。
在了解这一点前,我们先来看看条件类型常用的一个场景:泛型约束,实际上就是我们上面的例子:
function pickSingleValue<T extends object, U extends keyof T>(
obj: T,
key: U
): T[U] {
return obj[key];
}
这里的T extends object
与U extends keyof T
都是泛型约束,分别将 T 约束为对象类型 和 将 U 约束为 T 键名的字面量联合类型。我们通常使用泛型约束来 收窄类型约束。
以一个使用条件类型作为函数返回值类型的例子:
declare function strOrNum(
x: T
): T extends true ? string : number;
在这种情况下,条件类型的推导就会被延迟,因为此时类型系统没有足够的信息来完成判断。
只有给出了所需信息(在这里是入参x的类型),才可以完成推导。
const strReturnType = strOrNum(true);
const numReturnType = strOrNum(false);
同样的,就像三元表达式可以嵌套,条件类型也可以嵌套,如果你看过一些框架源码,也会发现其中存在着许多嵌套的条件类型,无他,条件类型可以将类型约束收拢到非常精确的范围内。
type TypeName = T extends string
? “string”
: T extends number
? “number”
: T extends boolean
? “boolean”
: T extends undefined
? “undefined”
: T extends Function
? “function”
: “object”;
分布式条件类型 Distributive Conditional Types
官方文档对分布式条件类型的讲解内容甚至要多于条件类型,因此你也知道这玩意没那么简单了吧~
分布式条件类型实际上不是一种特殊的条件类型,而是其特性之一。先上概念:对于属于裸类型参数的检查类型,条件类型会在实例化时期自动分发到联合类型上
原文: Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation
先提取几个关键词,然后我们再通过例子理清这个概念:
-
裸类型参数
-
实例化
-
分发到联合类型
// 使用上面的TypeName类型别名
// “string” | “function”
type T1 = TypeName<string | (() => void)>;
// “string” | “object”
type T2 = TypeName<string | string[]>;
// “object”
type T3 = TypeName<string[] | number[]>;
我们发现在上面的例子里,条件类型的推导结果都是联合类型(T3 实际上也是,只不过相同所以被合并了),并且其实就是类型参数被依次进行条件判断后,再使用|
组合得来的结果。
是不是 get 到了一点什么?我们再看另一个例子:
type Naked = T extends boolean ? “Y” : “N”;
type Wrapped = [T] extends [boolean] ? “Y” : “N”;
/*
* 先分发到 Naked | Naked
* 所以结果是"N" | “Y”
*/
type Distributed = Naked<number | boolean>;
/*
* 不会分发 直接是 [number | boolean] extends [boolean]
* 这样当然就是"N"啦~
*/
type NotDistributed = Wrapped<number | boolean>;
现在我们可以来讲讲这几个概念了:
-
裸类型参数,没有额外被接口/类型别名/奇怪的东西包裹过的,就像被
Wrapped
包裹后就不能再被称为裸类型参数。 -
实例化,其实就是条件类型的判断过程,就像我们前面说的,条件类型需要在收集到足够的推断信息之后才能进行这个过程。在这里两个例子的实例化过程实际上是不同的,具体会在下一点中介绍。
-
分发至联合类型的过程:
-
- 对于 TypeName,它内部的类型参数 T 是没有被包裹过的,所以
TypeName<string | (() => void)>
会被分发为TypeName<string> | TypeName<(() => void)>
,然后再次进行判断,最后分发为"string" | "function"
。
- 对于 TypeName,它内部的类型参数 T 是没有被包裹过的,所以
-
抽象下具体过程:
( A | B | C ) extends T ? X : Y
// 相当于
(A extends T ? X : Y) | (B extends T ? X : Y) | (B extends T ? X : Y)
一句话概括:没有被额外包装的联合类型参数,在条件类型进行判定时会将联合类型分发,分别进行判断。
infer 关键字
infer
是inference
的缩写,通常的使用方式是infer R
,R
表示 待推断的类型。如果说,通常infer
不会被直接使用,而是与条件类型一起,被放置在底层工具类型中,用于
看一个简单的例子,用于获取函数返回值类型的工具类型ReturnType
:
const foo = (): string => {
return “linbudu”;
};
// string
type FooReturnType = ReturnType;
infer
的使用思路可能不是那么好习惯,我们可以用前端开发中常见的一个例子类比,页面初始化时先显示占位交互,像 Loading/骨架屏,在请求返回后再去渲染真实数据。infer
也是这个思路,类型系统在获得足够的信息后,就能将 infer 后跟随的类型参数推导出来,最后返回这个推导结果。
type ReturnType = T extends (…args: any[]) => infer R ? R : any;
-
(...args: any[]) => infer R
是一个整体,这里函数的返回值类型的位置被infer R
占据了。 -
当
ReturnType
被调用,泛型T被实际类型填充,如果T满足条件类型的约束,就返回R的值,在这里R即为函数的返回值实际类型。 -
实际上为了严谨,应当约束泛型T为函数类型,即:
type ReturnType<T extends (…args: any) => any> = T extends (…args: any) => infer R ? R : any;
类似的,借着这个思路我们还可以获得函数入参类型、类的构造函数入参类型、甚至Promise 内部的类型等,这些工具类型我们会在后面讲到。
infer 其实没有特别难消化的知识点,它需要的只是思路的转变,你要理解 延迟推断 的概念。
类型守卫 与 is in 关键字 Type Guards
前面的内容可能不是那么符合人类直觉,需要一点时间消化,这一节我们来看点简单(相对)且直观的知识点:类型守卫。
假设有这么一个字段,它可能字符串也可能是数字:
numOrStrProp: number | string;
现在在使用时,你想将这个字段的联合类型缩小范围,比如精确到string
,你可能会这么写:
export const isString = (arg: unknown): boolean => typeof arg === “string”;
看看这么写的效果:
function useIt(numOrStr: number | string) {
if (isString(numOrStr)) {
console.log(numOrStr.length);
}
}
image
啊哦,看起来isString
函数并没有起到缩小类型范围的作用,参数依然是联合类型。这个时候就该使用is
关键字了:
export const isString = (arg: unknown): arg is string =>
typeof arg === “string”;
这个时候再去使用,就会发现在isString(numOrStr)
为 true 后,numOrStr
的类型就被缩小到了string
。这只是以原始类型为成员的联合类型,我们完全可以扩展到各种场景上,先看一个简单的假值判断:
export type Falsy = false | “” | 0 | null | undefined;
export const isFalsy = (val: unknown): val is Falsy => !val;
是不是还挺有用?这应该是我日常用的最多的类型别名之一了。
也可以在 in 关键字的加持下,进行更强力的类型判断,思考下面这个例子,要如何将 " A | B " 的联合类型缩小到"A"?
class A {
public a() {}
public useA() {
return “A”;
}
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
总结
我在成长过程中也是一路摸爬滚打,没有任何人的指点,所以走的很艰难。例如在大三的时候,如果有个学长可以阶段性的指点一二,如果有已经工作的师兄可以告诉我工作上需要什么,我应该前面的三年可以缩短一半;后来去面试bat,失败了有5、6次,每次也不知道具体是什么原因,都是靠面试回忆去猜测可能是哪方面的问题,回来学习和完善,当你真正去招人的时候,你就会知道面试记录是多么重要,面试官可以从面试记录里看到你的成长,总是去面试,总是没有成长,就会被定义为缺乏潜力。
CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算
78)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-F0BVDjrj-1712321286078)]
总结
我在成长过程中也是一路摸爬滚打,没有任何人的指点,所以走的很艰难。例如在大三的时候,如果有个学长可以阶段性的指点一二,如果有已经工作的师兄可以告诉我工作上需要什么,我应该前面的三年可以缩短一半;后来去面试bat,失败了有5、6次,每次也不知道具体是什么原因,都是靠面试回忆去猜测可能是哪方面的问题,回来学习和完善,当你真正去招人的时候,你就会知道面试记录是多么重要,面试官可以从面试记录里看到你的成长,总是去面试,总是没有成长,就会被定义为缺乏潜力。
CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
[外链图片转存中…(img-Tvbe72Ur-1712321286079)]
[外链图片转存中…(img-4iqO2Dw5-1712321286079)]
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算