Typescript:通用类型“提取具有 X 类型值的键”的行为不符合预期

2023-12-07

我定义了以下泛型类型,它从类型 T 中提取值为数字的字符串键:

type StringKeysMatchingNumber<T> = {
  [K in keyof T]-?: K extends string ?
    T[K] extends number ?
      K
      : never
    : never
}[keyof T];

我尝试在通用函数中使用这种类型,如下所示:

function setNumberField<T>(item: T, field: StringKeysMatchingNumber<T>): void {
  item[field] = 1;
}

但线item[field] = 1;错误与Type 'number' is not assignable to type 'T[StringKeysMatchingNumber<T>]'.

我尝试了一些不同的方法,例如将函数中的泛型类型 T 缩小为显式包含一些带有值编号的字符串键的类型,但这没有帮助。

任何人都可以看到问题是什么?这是一个 TS 游乐场,其中包含代码示例和更多详细信息:https://www.typescriptlang.org/play?#code/C4TwDgpgBAKhDOwbmgXigbwLACgpQEMAuKAOwFcBbAIwgCcAaXfagfhIpvqbygGMSiOgEtSAcx74AJhyq06uAL65cAelVQAgvCgQAHpD7AIU2AmABpCCB3oARATtQAPlDtS7uUJ DOIrNqHQAZWARcX94AFkCYD4AC1ExADk5egAeOERkSAA+FRxvaBCwsQjo2ITxFK46DJzAzGYoAG0LKFEoAGtrAHsAM1gAXQBadig2-WNSKR0hRKhWJvwYVsHDPSmZslS6BaX8cf38DggAN3p9k-OFHEVm7pB+ oYBufL7yUiNhHtIoeAhgNV5AAxYQQAA2UjqAAphMZKCQYAwoH0wZCSMVEmUYvFEkD0jAcgBKEinHrCUzYXhwiCUZqoiFSNboACMr1uQA


T in setNumberField是一个黑匣子。没有人知道,甚至你,是否T是否有带数值的键。没有适当的约束。setNumberField允许您提供原始值作为第一个参数。这意味着在函数体内,TS 不知道item[field]始终是一个数值。然而,TS 在函数调用期间意识到了这一点。所以函数有两个级别的类型。一 - 是函数定义,当 TS 无法猜测时T类型和第二个 - 在函数调用期间,当 TS 知道时T类型并能够推断它。

最简单的方法就是避免突变。您可以返回新对象。 考虑这个例子:

type TestType = {
  a: number,
  b?: number,
  c: string,
  d: number
}

type StringKeysMatchingNumber<T> = {
  [K in keyof T]-?: K extends string ?
  T[K] extends number ?
  K
  : never
  : never
}[keyof T];

const setNumberField = <
  Item,
  Field extends StringKeysMatchingNumber<Item>
>(item: Item, field: Field): Item => ({
  ...item,
  [field]: 1
})

declare let foo: TestType

// {
//     a: number;
//     b: string;
// }
const result = setNumberField({ a: 42, b: 'str' }, 'a')

操场

请记住,TypeScript 不喜欢突变。看我的article


如果你仍然想改变你的参数,你应该重载你的函数。

type TestType = {
  a: number,
  b?: number,
  c: string,
  d: number
}

type StringKeysMatchingNumber<T> = {
  [K in keyof T]-?: K extends string ?
  T[K] extends number ?
  K
  : never
  : never
}[keyof T];

function setNumberField<Item, Field extends StringKeysMatchingNumber<Item>>(item: Item, field: Field): void;
function setNumberField(item: Record<string, number>, field: string): void {
  item[field] = 2
}

declare let foo: TestType

const result1 = setNumberField({ a: 42, b: 'str' }, 'a') // ok
const result2 = setNumberField({ a: 42, b: 'str' }, 'b') // expected error

操场

函数重载并不那么严格。您可能已经注意到,这个函数类型定义function setNumberField(item: Record<string, number>, field: string)允许您仅使用所有值都是数字的对象。但这种情况并非如此。这就是为什么我用另一层重载了这个函数。最下面的一个用于函数体。最上面的一张,带有StringKeysMatchingNumber控制函数参数。


UPDATE

为什么要添加约束,例如T extends Record<string, number>是 不足以让 TS 知道其类型item[field]

考虑一下:

type StringKeysMatchingNumber<T> = {
  [K in keyof T]-?: K extends string ?
  T[K] extends number ? // This line does not mean that T[K] is equal to number
  K
  : never
  : never
}[keyof T];

这条线T[K] extends number意思是T[K]是数字的子类型。有可能number & {__tag:'Batman'}。另外,请记住,StringKeysMatchingNumber可能会回来never并且编号不可分配给never:

declare let x: never;
x = 1 // error

请注意,那个呼唤StringKeysMatchingNumber与静态参数一样{foo: 42}产生预期结果"foo":

type Result = StringKeysMatchingNumber<{ foo: 42 }> // foo

但解决StringKeysMatchingNumber函数体内部是完全不同的历史。将鼠标悬停在Result内部函数 参见示例:

function setNumberField<
  T extends Record<string, number>,
  Field extends StringKeysMatchingNumber<T>
>(item: T, field: Field) {

  type Result = StringKeysMatchingNumber<T> // resolving T inside a function

  const value = item[field];

  item[field] = 1; // error
  value.toExponential // ok
}

item[field]决心T[Field],它不是一个number类型。它是一个子类型number类型。您仍然可以致电toExponential。了解这一点非常重要item[field]是一种具有以下所有属性的类型number类型,但也可能包含一些其他属性。number从 TS 的角度来看并不原始。


//"toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"
type NumberKeys = keyof number

看看这个:


function setNumberField<
  T extends Record<string, number>,
  Field extends StringKeysMatchingNumber<T>
>(item: T, field: Field) {
  let numSupertype = 5;
  let numSubtype = item[field]
  
  numSupertype = numSubtype // ok
  numSubtype = numSupertype // expected error
}

numSubtype可分配给numSupertype. item[field]可分配给任何变量number键入而number不可分配给item[field].

最后一个问题是您想要分配号码的方式item[field]类型足够安全吗?

type StringKeysMatchingNumber<T> = {
  [K in keyof T]-?: T[K] extends number ? K : never
}[keyof T];


function setNumberField<

  T extends Record<string, number>,
  Field extends StringKeysMatchingNumber<T>
>(item: T, field: Field) {
  item[field] = 2
}

type BrandNumber = number & { __tag: 'Batman' }

declare let brandNumber: BrandNumber
type WeirdDictionary = Record<string, BrandNumber>

const obj: WeirdDictionary = {
  property: brandNumber
}

setNumberField(obj, 'foo')

setNumberField需要一个字典,其中每个值都扩展number类型。这意味着价值可能是number & { __tag: 'Batman' }。我知道,从开发人员的角度来看这很奇怪,但从类型的角度来看却并不奇怪。它只是一个子类型number该技术用于模拟名义类型。 如果您分配会发生什么2 to item[field]没有错误?调用此函数后,您期望每个值是BrandNumber但这不会是真的。

所以,TypeScript 在这里做得很好:D

您可以在我的文章中找到有关函数参数推断的更多信息article

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Typescript:通用类型“提取具有 X 类型值的键”的行为不符合预期 的相关文章

随机推荐

  • “resolve_variable”在 Django 中做什么? (“模板.变量”)

    什么是resolve variable做 我可以用它来访问request视野之外 Edit So template Variable是正确的方法 但我仍然不确定其目的 该文档并没有真正的帮助 干杯 伙计们 我假设您尝试在此处编写自定义模板标
  • 关闭 pyplot 窗口

    最终编辑 我在关闭 pyplot 窗口的主题上发现 它实际上可能不应该使用 pyplot 来完成 SRK 给出了一个关于如何处理绘图的很好的例子 这些例子将在下面的答案中更新 另外 我还偶然发现了如何将 pyplot 绘图放入 Tkinte
  • 如果没有循环,这个 JavaScript 问题是否可能实现?

    目前在代码提交网站上 它不会让我继续前进 使用此字符数组 将名称以 M 开头的每个字符打印到控制台 暂时不要使用任何类型的循环 var filmCharacters Vito Michael Sonny Freddo Mia Vincent
  • 如何声明一个可以被每个方法使用的变量? | C#

    我想问一下如何声明一个可以被所有方法使用的变量 我尝试将方法的访问类型公开 但这不允许我在其他方法中使用它的变量 此外 我基本上想通过不同的方法累积具有不同值的变量 这就是我问这个的原因 NOTE 我想避免创建任何静态类 EDIT 例如 我
  • 我如何知道 Flex DataGrid itemRenderer 中的按钮何时被单击?

    我有一个显示几列数据的 DataGrid 组件 它还有一个附加列 显示一个按钮 允许用户对记录执行操作
  • Pandas 中通过多个分隔符将一列拆分为多列

    给定一个数据框如下 player score 0 Sergio Ag ero Forward Manchester City 209 98 1 Eden Hazard Midfield Chelsea 274 04 2 Alexis S n
  • MongoDB-CR 身份验证失败

    我在验证用户身份时收到以下错误 purchase user purchase 失败 MongoDB CR 身份验证失败 当我通过浏览器访问 Web 服务时 用户文档中缺少凭据 但我能够从 mongo 验证 buy user 它返回 1 转到
  • 在 WooCommerce 中将爱尔兰的邮政编码结帐字段设置为必填字段

    在 WooCommerce 结帐页面中 我希望将邮政编码字段 爱尔兰爱尔兰代码 更改为必填字段 默认情况下 当所选国家 地区为爱尔兰时 这是可选字段 我只是想知道是否有办法将其更改为必填字段 并在 Eircode 旁边有一个小红星来表示它是
  • 如何在R中快速组合60个矩阵

    我在 R 中有 60 个矩阵 名为 mat1 mat2 mat60 我想使用 rbind 将它们组合成一个大矩阵 我知道我可以写一些类似的东西 matList lt list mat1 mat2 mat60 rbind matList 但这
  • 如何使用FileSavePicker保存现有的StorageFile?

    我正在尝试将现有文件保存到另一个地方 这是某种副本 但我想允许用户使用 FileSavePicker 选择新的目的地 这是我的代码 StorageFile currentImage await StorageFile GetFileFrom
  • 我可以在 Windows XP SP2 上使用哪些免费的 Powershell 编辑器?

    我家里有 Windows 7 和 Powershell 2 我非常喜欢免费版本的 PowerGUI Powershell 编辑器 我想在工作中开始使用 Powershell 但我们使用的是 XP SP2 上的 1 o 版本 我本来打算安装
  • 对 2 个相互依赖的表进行复杂的 SQL 更新

    我有一个数据库 其中有几个表跟踪电话 短信 数据和津贴 我正在尝试计算是否可以在不诉诸游标的情况下将电话分配给津贴 但我无法找到一种方法构造 SQL 来执行此操作 我的尝试没有得到任何有用的 SQL 因为我似乎不知道如何处理它 问题是 对我
  • lxml xpath 不会忽略“ ”

    我有这个 HTML td class 0 b Bold Text b nbsp a href a td td class 0 Regular Text nbsp a href a td 当使用 xpath 格式化时 new html tre
  • Android、Java 上的单选按钮列表

    我需要创建一个单选按钮列表 但如果我尝试使用 ListView 来完成此操作 我可以同时选择所有单选按钮 但我需要同时选择 1 个单选按钮 因此 我可以尝试这段代码 RadioGroup group new RadioGroup this
  • 使用 FCM 从服务器发送推送通知

    最近我问了一个关于使用GCM发送推送通知的问题 向 Android 发送推送通知 现在有了FCM 我想知道它与服务器端开发有什么不同 就编码而言 它们相同吗 在哪里可以找到显示从服务器向 Android 设备发送推送通知的示例 FCM 代码
  • 有没有办法在 Lightswitch 中基于下拉菜单创建搜索屏幕

    我想在搜索屏幕上的搜索框所在位置放置一个下拉列表 并将每个条目与不同的查询相关联 这是针对我的业务中使用的数据库 我们目前有太多屏幕 例如 所有设备 新设备 可用设备 出售设备 未付费设备 未付佣金 我正在使用 2012 RC 的 Ligh
  • 字符串重复被连字符替换C++

    我是编码的初学者 正在尝试这个问题 用连字符替换字符串中字母的所有重复 即 ABCDAKEA 将变为 ABCD KE 我使用了 switch 循环并且它有效 但我想让它更短 也许使用递归来使其更有效 有任何想法吗 include
  • iOS 绘制两端弯曲的矩形

    想象一个长矩形 尺寸可能为 200x20 所有侧面都有直边 在我的 iOS 应用程序中 这对我来说很容易绘制 CGContextFillRect context CGRectMake xLoc yLoc 200 20 现在 如果我希望较短的
  • 如何使用 Android 从 NFC 标签检测制造商?

    我正在尝试检测 NFC 标签的制造商名称 我找到了制造商代码here 但我只能使用这些 ID 检测 Mifare Ultralight C 标签 Ultralight C 标签 ID 的第一个字节代表制造商代码 但其他标签不包含此属性 Th
  • Typescript:通用类型“提取具有 X 类型值的键”的行为不符合预期

    我定义了以下泛型类型 它从类型 T 中提取值为数字的字符串键 type StringKeysMatchingNumber