我如何提高 Typescript 技能 #2:映射类型
我将和大家分享一些提升 Typescript 技能的技巧!今天我们将学习如何使用Mapped Type
!
什么是映射类型以及为什么使用映射类型?
在以下情况下使用mapped type
很有用:
需要从另一个类型创建派生类型并保持两者同步
让我们看一个例子来理解!
type User = {
name: string
age: number
userName: string
}
type UserPermissions = {
canUpdateName: boolean
canUpdateAge: boolean
canUpdateUserName: boolean
}
User
这里我们可以看到&类型之间有相似之处UserPermissions
。有什么解决方案可以将这两种类型联系起来吗?因为我们不想UserPermissions
每次在类型中添加新属性时都手动添加属性User
。
答案是yes
,解决方案是Mapped type
!
映射类型:基础知识
创建相同类型
在回答上述问题之前,让我们看看映射类型是如何工作的!
我们将从User
类型创建一个类型,首先让我们创建完全相同的类型。
type User = {
name: string
age: number
userName: string
}
type UserCopy = {
[Property in keyof User]: User[Property]
}
如果你检查Usercopy
类型你会看到这个
type UserCopy = {
name: string;
age: number;
userName: string;
}
但是我们为什么要这样做,我们创建与User
?!相同的类型。
我选了一个简单的例子来演示Mapped 类型的工作原理。我会尽量解释清楚到底是怎么回事。
type UserCopy = {
[Property in keyof User]: User[Property]
}
首先,我们使用运算符迭代User
类型中的每个键(使用映射类型时经常使用),每个键都包含在变量中(,,)in keyof
Property
'name'
'age'
'userName'
之后,我们为每个属性赋值,并将 中的值赋给它们User[Property]
。直观地看,我们会得到类似这样的效果:
1)开始:[Property in keyof User]: User[Property]
-
迭代到 User 的第一个属性:
[name]: User['name']
,因此我们得到以下输入name: string
-
迭代到 User 的第二个属性:
[age]: User['age']
,因此我们得到以下输入age: number
-
迭代到 User 的最后一个属性:
[userName]: User['userName']
,因此我们得到以下输入userName: string
User
2)迭代每个键之后,我们得到一个与!类型相同的新类型。
编辑不同类型的值
我们还可以编辑属性值类型,例如:
type UserCopy = {
[Property in keyof User]: boolean
}
并得到类似
type UserCopy = {
name: boolean;
age: boolean;
userName: boolean;
}
我们开始见识Mapped 类型的威力了!如果我们在类型中添加一个新属性User
,它将自动添加到该UserCopy
类型中,无需执行任何操作!别忘了,开发人员很懒 ;)
映射类型:映射修饰符
从现在开始玩 Mapped 类型吧!
我们将使用mapping modifier
映射类型。
如果我需要创建以下类型UserReadonlyProperty
,即每个键上都User
带有readonly
标志的类型。我可以创建类似的东西。
type User = {
name: string
age: number
userName: string
}
type UserCopy = {
readonly [Property in keyof User]: User[Property];
};
// type UserCopy = {
// readonly name: string;
// readonly age: number;
// readonly userName: string;
// }
哇,真有意思!我们直接readonly
给每个键分配了标志!我们也可以反其道而行之!只需要-
在 readonly 之前使用,就能从类型中删除所有 readonly 标志了~
type User = {
readonly name: string
readonly age: number
readonly userName: string
}
type UserCopy = {
-readonly [Property in keyof User]: User[Property];
};
// We Get this 👇
type UserCopy = {
name: string;
age: number;
userName: string;
}
这就是所谓的mapping modifier
,我们还有另一个映射修饰符需要向您展示,即Optional
映射修饰符(?:
)。
type User = {
name: string
age: number
userName: string
}
type UserCopy = {
[Property in keyof User]?: User[Property];
};
// We Get this 👇
type UserCopy = {
name?: string | undefined;
age?: number | undefined;
userName?: string | undefined;
}
如您所见,我们可以在键上添加“readonly”和“optionnal”标志,但我们也可以删除它们!通过使用(-
)标志:[Property in keyof User]-?: User[Property]
type User = {
name?: string
age?: number
userName: string
}
type UserCopy = {
[Property in keyof User]-?: User[Property];
};
// We Get this 👇
type UserCopy = {
name: string;
age: number;
userName: string;
}
最后我们可以将两者结合起来🔥
type User = {
name: string
age: number
userName: string
}
type UserCopy = {
readonly [Property in keyof User]?: User[Property];
};
// We Get this 👇
type UserCopy = {
readonly name?: string | undefined;
readonly age?: number | undefined;
readonly userName?: string | undefined;
}
映射类型:键重新映射as
我们看到了mapping modifier
,这是一个与映射类型结合的非常好的特性!现在让我们更深入地了解一下mapped type
文字字符串
我们可以使用文字字符串重新映射键!
type User = {
name: string
age: number
userName: string
}
type RenameKey<Type> = {
[Property in keyof Type as `canUpdate${string & Property}`]: Type[Property]
}
type UserCopy = RenameKey<User>
// We Get this 👇
type UserCopy = {
canUpdatename: string;
canUpdateage: number;
canUpdateuserName: string;
}
使用as
许可证来更改密钥值并将其保留在新值中!
让我们一步一步地解构合成。
1)我们遍历Type
键,因此我们得到name
,age
和userName
。
2)在每次迭代中,我们将键值重命名为canUpdate${Property}
,因此我们得到
canUpdatename
canUpdateage
canUpdateuserName
2.5) 如果我们仔细观察 Syntax,就能看到{string & Property}
Syntax。为什么这里需要使用string
Value 呢?我会写一篇专门的文章来解释这个问题,但总结一下,我们需要告诉 TS 键是 astring
而不是 a symbol
,因为我们不能在字面量中使用symbol
字符串(其他键类型,例如数字或 bingint,则需要使用字符串)。
如你所见,与映射类型结合使用时,重新映射键的功能非常强大。但这里有一个“小”问题,我们得到了canUpdatename
not canUpdateName
。
我们怎样才能实现这个目标?🧐
内在字符串
为了帮助字符串操作,TypeScript 包含一组可用于字符串操作的类型,我们现在将使用它来转换canUpdatename
为canUpdateName
。
type User = {
name: string
age: number
userName: string
}
type Copy<Type> = {
[Property in keyof Type as `canUpdate${Capitalize<string & Property>}`]: Type[Property];
};
type UserCopy = Copy<User>
// We Get this 👇
type UserCopy = {
canUpdateName: string;
canUpdateAge: number;
canUpdateUserName: string;
}
我们做到了!使用intrinsic string
允许我们将字符串值大写!这里我们将 type 中的 Key 值大写User
。
我们还可以使用intrinsic string
映射类型上下文。
type LowercaseGreeting = "hello, world"
type Greeting = Capitalize<LowercaseGreeting> //Hello, world
这里还有其他的内在字符串
类型实用程序
我再重复一遍,但是Mapped type
功能强大,我们可以重命名一种类型的每个键来创建另一种类型,我们可以用它intrinsic string
来操作字符串。
还有其他事情要与他们有关;)
我们可以使用 Type Utils!(如果你不知道这是什么,请查看我上一篇关于 Typescript 技巧的文章,或者点击此处)
如果我们需要从类型中排除一组键来创建转换类型,我们可以结合使用mapped type
Type Utils !!
type User = {
name: string
age: number
userName: string
}
type CopyWithoutKeys<Type, Keys> = {
[Property in keyof Type as Exclude<Property , Keys>]: Type[Property];
};
type UserCopyWithoutNameAndUsername = CopyWithoutKeys<User, 'name' | 'userName'>
// We Get this 👇
type UserCopyWithoutNameAndUsername = {
age: number;
}
我想你已经理解了 Mapped 类型的语法和概念。当然,我们可以使用之前见过的所有功能!
奖励:从映射类型中获取所有值类型
对于读到这一节的所有人而言,这都是一个奖励。
您可以从映射类型(和类型)中获取所有键的值类型
type User = {
name: string
age: number
userName: string
isAdmin: boolean
}
type CopyWithoutKeys<Type> = {
[Property in keyof Type]: Type[Property];
};
type UserCopy = CopyWithoutKeys<User>
type UserCopyValueTypes = UserCopy[keyof UserCopy] // string | number | boolean
请注意,我添加了isAdmin
key 而没有在UserCopy
type 中添加它,这就是神奇之处Mapped Type
;)
映射类型的高级概念
永远不会作为键/对中的值
让我们看看当您使用 Never 作为映射类型的键/值时会发生什么!
Never 作为映射类型的键
如果在使用映射类型进行案例时在键迭代期间添加条件never
,它将产生一些很酷的东西
type isA<T> = T extends 'a' ? never : T
type To<T> = {
[P in keyof T as isA<P>]: T[P]
}
type Toto = To<{ a: 1; b: 2; c: 3 }> // { b: 2; c: 3; }
当您设置一个带有值的键时never
,它将从最终类型中被省略!
Never 作为映射类型的值
正如我们之前所见,使用never
可以产生一些关于映射类型的有用技巧,但是如果您将其用作never
值,会发生什么?
type To<T> = {
[P in keyof T]: T[P] extends never ? never : P
}
type Toto = To<{ a: 1; b: 2; c: never }> // {a: "a" b: "b" c: never;}
正如您所见,never
值并不Omit
像never
键。
使用never
值可以根据条件删除某些键/值,因此如果您需要这样做,请在key
级别使用您的条件!
// type PropertyKey = string | number | symbol
type isA<T extends Record<PropertyKey, unknown>, P extends PropertyKey> = T[P] extends 1 // Your condition
? never : P
type To<T extends Record<PropertyKey, unknown>> = {
[P in keyof T as isA<T,P>]: T[P]
}
type Toto = To<{ a: 1; b: 2; c: 2 }> // {b: 2;c: 2;}
在上面的例子中,我们删除所有值为 1 的键/值,我们在级别添加条件key
,因此never
值将自动Omit
!
具有值的联合类型
如果您对此类错误有任何问题
type Colors = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];
const palette: Record<Colors, string | RGB> = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255]
};
const redComponent = palette.red.at(0); // Property 'at' does not exist on type 'string | RGB'.
你应该阅读我上一篇关于satisfies
运算符的文章!
我希望你喜欢这篇文章!
☕️ 你可以支持我的作品🙏
🏃♂️ 你可以关注我 👇
🕊 推特:https://twitter.com/code__oz
👨💻 Github:https://github.com/Code-Oz
🇫🇷🥖 对于法国开发者,你可以查看我的YoutubeChannel
你也可以标记🔖这篇文章!
文章来源:https://dev.to/codeoz/how-i-improve-my-skills-in-typescript-2-mapped-type-dag