我如何提高 Typescript 技能 #2:映射类型

2025-05-25

我如何提高 Typescript 技能 #2:映射类型

我将和大家分享一些提升 Typescript 技能的技巧!今天我们将学习如何使用Mapped Type

什么是映射类型以及为什么使用映射类型?

在以下情况下使用mapped type很有用:

需要从另一个类型创建派生类型并保持两者同步

让我们看一个例子来理解!

type User = {
  name: string
  age: number
  userName: string
}

type UserPermissions = {
  canUpdateName: boolean
  canUpdateAge: boolean
  canUpdateUserName: boolean
}
Enter fullscreen mode Exit fullscreen mode

User这里我们可以看到&类型之间有相似之处UserPermissions。有什么解决方案可以将这两种类型联系起来吗?因为我们不想UserPermissions每次在类型中添加新属性时都手动添加属性User

答案是yes,解决方案是Mapped type

映射类型:基础知识

创建相同类型

在回答上述问题之前,让我们看看映射类型是如何工作的!

我们将从User类型创建一个类型,首先让我们创建完全相同的类型。

type User = {
  name: string
  age: number
  userName: string
}

type UserCopy = {
  [Property in keyof User]: User[Property]
}
Enter fullscreen mode Exit fullscreen mode

如果你检查Usercopy类型你会看到这个

type UserCopy = {
    name: string;
    age: number;
    userName: string;
}
Enter fullscreen mode Exit fullscreen mode

但是我们为什么要这样做,我们创建与User?!相同的类型。

我选了一个简单的例子来演示Mapped 类型的工作原理。我会尽量解释清楚到底是怎么回事。

type UserCopy = {
  [Property in keyof User]: User[Property]
}
Enter fullscreen mode Exit fullscreen mode

首先,我们使用运算符迭代User类型中的每个键(使用映射类型时经常使用),每个键都包含在变量中,,in keyofProperty'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

User2)迭代每个键之后,我们得到一个与!类型相同的新类型。

编辑不同类型的值

我们还可以编辑属性值类型,例如:

type UserCopy = {
  [Property in keyof User]: boolean
}
Enter fullscreen mode Exit fullscreen mode

并得到类似

type UserCopy = {
    name: boolean;
    age: boolean;
    userName: boolean;
}
Enter fullscreen mode Exit fullscreen mode

我们开始见识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;
// }
Enter fullscreen mode Exit fullscreen mode

哇,真有意思!我们直接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;
}
Enter fullscreen mode Exit fullscreen mode

这就是所谓的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;
}
Enter fullscreen mode Exit fullscreen mode

如您所见,我们可以在键上添加“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;
}
Enter fullscreen mode Exit fullscreen mode

最后我们可以将两者结合起来🔥

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;
}
Enter fullscreen mode Exit fullscreen mode

映射类型:键重新映射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;
}
Enter fullscreen mode Exit fullscreen mode

使用as许可证来更改密钥值并将其保留在新值中!

让我们一步一步地解构合成。

1)我们遍历Type键,因此我们得到nameageuserName

2)在每次迭代中,我们将键值重命名为canUpdate${Property},因此我们得到

  • canUpdatename
  • canUpdateage
  • canUpdateuserName

2.5) 如果我们仔细观察 Syntax,就能看到{string & Property}Syntax。为什么这里需要使用stringValue 呢?我会写一篇专门的文章来解释这个问题,但总结一下,我们需要告诉 TS 键是 astring而不是 a symbol,因为我们不能在字面量中使用symbol字符串(其他键类型,例如数字或 bingint,则需要使用字符串)。

如你所见,与映射类型结合使用时,重新映射键的功能非常强大。但这里有一个“小”问题,我们得到了canUpdatenamenot canUpdateName

我们怎样才能实现这个目标?🧐

内在字符串

为了帮助字符串操作,TypeScript 包含一组可用于字符串操作的类型,我们现在将使用它来转换canUpdatenamecanUpdateName

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;
}
Enter fullscreen mode Exit fullscreen mode

我们做到了!使用intrinsic string允许我们将字符串值大写!这里我们将 type 中的 Key 值大写User

我们还可以使用intrinsic string映射类型上下文。

type LowercaseGreeting = "hello, world"
type Greeting = Capitalize<LowercaseGreeting> //Hello, world
Enter fullscreen mode Exit fullscreen mode

这里还有其他的内在字符串

类型实用程序

我再重复一遍,但是Mapped type功能强大,我们可以重命名一种类型的每个键来创建另一种类型,我们可以用它intrinsic string来操作字符串。

还有其他事情要与他们有关;)

我们可以使用 Type Utils!(如果你不知道这是什么,请查看我上一篇关于 Typescript 技巧的文章,或者点击此处

如果我们需要从类型中排除一组键来创建转换类型,我们可以结合使用mapped typeType 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;
}
Enter fullscreen mode Exit fullscreen mode

我想你已经理解了 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
Enter fullscreen mode Exit fullscreen mode

请注意,我添加了isAdminkey 而没有在UserCopytype 中添加它,这就是神奇之处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; }
Enter fullscreen mode Exit fullscreen mode

当您设置一个带有值的键时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;}
Enter fullscreen mode Exit fullscreen mode

正如您所见,never值并不Omitnever键。

使用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;}
Enter fullscreen mode Exit fullscreen mode

在上面的例子中,我们删除所有值为 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'.
Enter fullscreen mode Exit fullscreen mode

你应该阅读我上一篇关于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
PREV
JavaScript 学院 #1:原始值 vs 引用值
NEXT
如何在 5 分钟内创建一个漂亮的 Github 个人资料页面!