令人震撼的🤯 TypeScript 技巧
一点小提示...
内置类型🥱
多汁的东西😋
抱歉标题太坑了🙃。不过这完全是出于好意,因为我要介绍一些 TypeScript 相关的技巧,它们一定会让你大吃一惊。如果你能读完整篇文章,对任何一个技巧都不感到吃惊,那就太好了!你已经是 TypeScript 高手了🥳
那么让我们直奔主题吧。
一点小提示...
本文的级别为高级。您可能不了解它们的工作原理。但是,您不必这样做。您只需复制粘贴代码片段,并了解如何使用它,因为它们会让您的工作变得轻松,并且随着时间的推移,您将掌握它们的实际工作原理。
内置类型🥱
这些是 TypeScript 中的一些内置辅助类型。我会尽量简短地介绍这些内容,因为你可以在任何地方阅读。TypeScript文档是一个不错的起点,之后我们再深入探讨一些精彩内容 😋
挑选
它允许从类型/接口中选择特定字段及其类型,并创建一个全新的类型。我们来看看这个 👇
type UserFields = {
id: number;
name: string;
gender: 'male' | 'female' | 'non-binary' | 'prefer-not-to-say';
dob: Date;
};
type NameAndGenderOnly = Pick<UserFields, 'name' | 'gender'>;
// This is equal to 👇
type NameAndGenderOnly = {
name: string;
gender: 'male' | 'female' | 'non-binary' | 'prefer-not-to-say';
};
看到了吗?!相同的类型,没有任何重复。
部分的
这是我最常用的类型。如果你有一个类型/接口,并且出于某种原因,你想让它的所有字段都可选,那就用这个吧👇
type UserFields = {
id: number;
name: string;
gender: 'male' | 'female' | 'non-binary' | 'prefer-not-to-say';
dob: Date;
};
type OptionalUserFields = Partial<UserFields>;
// This is equal to 👇
type OptionalUserFields = {
id?: number;
name?: string;
gender?: 'male' | 'female' | 'non-binary' | 'prefer-not-to-say';
dob?: Date;
};
只读
当你想确保对象的属性在代码中无法更改时,这非常有用。可以将其视为const
对象属性的。
type UserFields = {
id: number;
name: string;
gender: 'male' | 'female' | 'non-binary' | 'prefer-not-to-say';
dob: Date;
};
const userData: Readonly<UserFields> = {
id: 100,
name: 'Puru Vijay',
gender: 'male',
dob: new Date('12 Nov, 2001'),
};
尝试修改任何属性userData.name = 'Hoolalala'
都会导致错误。
记录
Record
现在我们开始聊聊好东西。最近,在开发我当前的项目macos.now.sh(Shameless Plug,它基本上是用 Preact 和 Vite 编写的 macOS Big Sur 克隆版)时,我对它产生了新的敬意。
看看这个👇
export type AppName =
| 'finder'
| 'launchpad'
| 'safari'
| 'messages'
| 'mail'
| 'maps'
| 'photos'
| 'facetime'
| 'calendar';
/** Which apps are currently open */
export const openApps: Record<AppName, boolean> = {
finder: false,
launchpad: false,
safari: false,
messages: false,
mail: false,
maps: false,
photos: false,
facetime: false,
calendar: false,
};
如您所见,这只是一个简单的键值对。但我希望强制此对象包含AppName
联合类型中列出的所有应用,并且所有值均为布尔值。此外,我还希望在向列表中添加新应用时出现错误提示,这将导致我将该应用的键值对添加到此openApps
对象中。
这就是Record
TypeScript 的用武之地。它只是一种强制键和值类型的方法。这是 TypeScript 添加的另一层安全保障。
多汁的东西😋
现在有趣的部分开始了。
从数组中检索元素类型
假设你有一个数组,你想从数组中提取每个元素的类型
type ArrayElement<
ArrayType extends readonly unknown[]
> = ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
我们infer
在这里使用 TypeScript,它有助于从复杂类型中挑选出特定类型。
使用方法如下:
type A = ArrayElement<string[]>; // string
type B = ArrayElement<readonly string[]>; // string
type C = ArrayElement<[string, number]>; // string | number
type D = ArrayElement<['foo', 'bar']>; // "foo" | "bar"
type E = ArrayElement<(P | Q | R)[]>; // P | Q | R
type Error1 = ArrayElement<{ name: string }>;
// ^^^^^^^^^^^^^^^^
// Error: Type '{ name: string; }' does not satisfy the constraint 'readonly unknown[]'.
有一个稍微简单的版本来获取元素类型。
type ArrayElement<ArrayType extends readonly unknown[]> = ArrayType[number];
从承诺中检索类型
有没有想过从返回 Promise 的函数中获取类型?你可能尝试过这样做:
function returnsPromise(): Promise<number>;
let num: typeof returnsPromise;
// ^^^^^^^^^^^^^^^^^^^^^
// num: () => Promise<number>
我们希望num
的类型成为承诺的返回类型(在本例中为number
),而上述解决方案肯定不起作用。
解决方案是再次使用infer
从承诺中检索类型:
type UnwrapPromise<T> = T extends (props: any) => PromiseLike<infer U>
? U
: T extends PromiseLike<infer K>
? K
: T;
用法:
function returnsPromise(props: any) {
return Promise.resolve(6);
}
const num: UnwrapPromise<typeof returnsPromise> = 8;
// num: number
这里我们将一个返回 Promise 的函数包装到了这个类型中。这Promise<unknown>
也可以直接用于常规类型。
为什么
PromiseLike
而不是Promise
? \
Promise
接口自带了许多 Promise 独有的预建方法。但有时,你想创建一个.then
像 Promise 一样返回 a 的函数,但又不具备 Promise 的所有属性Promise
。在这种情况下,我们使用PromiseLike
补充一下:你可以重命名UnwrapPromise
为BreakPromise
。这不会影响代码,但能让人开心一下 🤣🤣
将元组转换为联合类型
这是一个元组:
const alphabets = ['a', 'b', 'c', 'd'] as const;
注意:如果没有
as const
结尾,typescript 会将类型解释为string[]
,而不是元组
现在我们想将这些特定的字符串用作联合类型。很简单。
type Alphabet = 'a' | 'b' | 'c' | 'd';
这样就行了。但是,假设这个类型和上面的数组最终会放在不同的文件中,而且项目变得很大,几个月后你回来,给变量添加了另一个值e
,alphabets
然后 BOOM!!! 整个代码库都崩溃了,因为你忘了添加e
联合Alphabet
类型。
我们可以自动化Alphabet
联合类型的生成,以便它直接从alphabets
变量中提取其成员。
type Alphabet = typeof alphabets[number];
这是通用类型安全助手:
type UnionFromTuple<Tuple extends readonly (string | number | boolean)[]> = Tuple[number];
用法:
const alphabets = ['a', 'b', 'c', 'd'] as const;
type Alphabet = UnionFromTuple<typeof alphabets>;
// type Alphabet = 'a' | 'b' | 'c' | 'd'
为什么
readonly array
? \
\
本节讲的是元组到联合类型的转换,但在代码中我们并没有使用这个词tuple
。原因是 tuple 不是关键字。就 TypeScript 而言,areadonly Array
就是一个元组。它没有Tuple
类型或其他信息。这就是为什么我要确保传递给它的类型UnionFromTuple
是元组,而不是数组。如果是数组,它基本上与上面我们从数组中检索元素类型的部分相同。
来自对象的联合类型
假设我们有这个对象:
const openApps = {
finder: false,
launchpad: false,
safari: false,
messages: false,
mail: false,
maps: false,
photos: false,
facetime: false,
calendar: false,
};
我想创建一个基于此处指定的键的联合类型。如果我向此对象添加一个额外的键值对,我希望联合类型也包含该键值对。
解决方案如下:
type KeysOfObject<T extends { [K in string | number]: unknown }> = keyof T;
用法👇
type App = KeysOfObject<typeof openApps>;
这将等于👇
type App =
| 'finder'
| 'launchpad'
| 'safari'
| 'messages'
| 'mail'
| 'maps'
| 'photos'
| 'facetime'
| 'calendar';
更好的 Object.Keys
这篇文章看起来就像是各种辅助类型的汇总,事实也确实如此。但在这篇文章中,我要分享一个技巧,它既不是最令人兴奋,也不是最酷炫的。它很无聊,但重要的是,它是整篇文章中最有用的技巧。如果你需要从这篇文章中学到什么,那就从这个技巧开始吧。除了这部分,其他的都可以忽略。
让我们看一下之前的对象:
const openApps = {
finder: false,
launchpad: false,
safari: false,
messages: false,
mail: false,
maps: false,
photos: false,
facetime: false,
calendar: false,
};
假设我想申请Object.keys
获取该对象的键数组。
const apps = Object.keys(openApps);
// ["finder", "launchpad", "safari", "messages", "mail", "maps", "photos", "facetime", "calendar"]
但这里有个小问题。如果你把鼠标悬停在 上apps
,它的类型会是string
[],而不是("finder" | "launchpad" | "safari" | "messages" | "mail" | "maps" | "photos" | "facetime" | "calendar")[]
。
从本质上来说,这并不是一个问题Object.keys
,但如果能返回键的联合类型数组就太好了。
Object.keys
那么让我们来调查一下这个问题。我们从预建的定义开始lib.d.ts
:
interface ObjectConstructor {
//...
keys(o: object): string[];
keys(o: {}): string[];
}
如果你觉得它
keys
被定义两次很奇怪,它被称为函数/方法重载。基本上,您可以定义多个函数声明以便灵活使用。
如你所见,它被硬编码为始终返回string[]
。我相信它存在是有充分理由的,但对我来说相当不方便,所以我将重写此方法,以便根据传递的内容正确推断出键。
如果您.d.ts
的项目中有一个根文件,请将下面的代码片段放入其中。
type ObjectKeys<Obj> = Obj extends object
? (keyof Obj)[]
: Obj extends number
? []
: Obj extends Array<any> | string
? string[]
: never;
interface ObjectConstructor {
keys<ObjectType>(o: ObjectType): ObjectKeys<ObjectType>;
}
现在让我们用新的尝试上面的代码Object.keys
:
const apps = Object.keys(openApps);
// const apps: ("finder" | "launchpad" | "safari" | "messages" | "mail" | "maps" | "photos" | "facetime" | "calendar")[]
不相信我?自己去TypeScript Playground看看吧。
注:所有功劳都归功于 Steven Baumgeitner 的博客文章,他讲的也是同样的道理。我只是抄袭了它😁。你可以
Object.keys
在他的博客文章中阅读更多关于修复的内容。
好了,就是这样了!!希望你从这篇博文中有所收获!
签字结束!!
文章来源:https://dev.to/puruvj/mindblowing-typescript-tricks-46nf