TypeScrip:泛型类型&提取具有类型X的值的键的行为不符合预期 [英] Typescript : Generic type "extract keys with value of type X" does not behave as expected

查看:11
本文介绍了TypeScrip:泛型类型&提取具有类型X的值的键的行为不符合预期的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我定义了以下泛型类型,它从类型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缩小为显式包含一些带有值Numbers的字符串键的类型,但这没有帮助。

有人能看到问题出在哪里吗?以下是一个包含代码样例和更多细节的TS游乐场:https://www.typescriptlang.org/play?#code/C4TwDgpgBAKhDOwbmgXigbwLACgpQEMAuKAOwFcBbAIwgCcAaXfagfhIpvqbygGMSiOgEtSAcx74AJhyq06uAL65cAelVQAgvCgQAHpD7AIU2AmABpCCB3oARATtQAPlDtS7uUJDOIrNqHQAZWARcX94AFkCYD4AC1ExADk5egAeOERkSAA+FRxvaBCwsQjo2ITxFK46DJzAzGYoAG0LKFEoAGtrAHsAM1gAXQBadig2-WNSKR0hRKhWJvwYVsHdPSmZslS6BaX8cf38DggAN3p9k-OFHEVm7pB+oYBufL7yUiNhHtIoeAhgNV5AAxYQQAA2UjqAAphMZKCQYAwoH0wZCSMVEmUYvFEkD0jAcgBKEinHrCUzYXhwiCUZqoiFSNboACMr1uQA

推荐答案

T中的T是一个黑盒。没有人知道,即使是您,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')

Playground

请记住,打字稿不喜欢突变。请参阅我的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

Playground

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


更新

为什么添加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]是Numbers的子类型。可以是number & {__tag:'Batman'}。另外,请记住,StringKeysMatchingNumber可能会返回never,并且数字不能分配给never

declare let x: never;
x = 1 // error
请注意,使用类似{foo: 42}静态参数调用StringKeysMatchingNumber会产生预期的结果"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类型的一个子类型。YUO仍然被允许呼叫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可赋值给numSupertypeitem[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分配给item[field]而没有出错,会发生什么情况?调用此函数后,您希望每个值都为BrandNumber,但实际情况并非如此。

因此,TypeScrip在这里做得很好:d

您可以在我的article

中找到有关函数参数推理的详细信息

这篇关于TypeScrip:泛型类型&提取具有类型X的值的键的行为不符合预期的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆