gpt4 book ai didi

typescript : Generic type "extract keys with value of type X" does not behave as expected

转载 作者:行者123 更新时间:2023-12-05 03:32:20 31 4
gpt4 key购买 nike

我定义了以下通用类型,它从类型 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 缩小为一种类型,该类型明确包含一些具有值 number 的字符串键,但这没有帮助。

谁能看出是什么问题?这是带有代码示例和更多详细信息的 TS Playground :https://www.typescriptlang.org/play?#code/C4TwDgpgBAKhDOwbmgXigbwLACgpQEMAuKAOwFcBbAIwgCcAaXfagfhIpvqbygGMSiOgEtSAcx74AJhyq06uAL65cAelVQAgvCgQAHpD7AIU2AmABpCCB3oARATtQAPlDtS7uUJDOIrNqHQAZWARcX94AFkCYD4AC1ExADk5egAeOERkSAA+FRxvaBCwsQjo2ITxFK46DJzAzGYoAG0LKFEoAGtrAHsAM1gAXQBadig2-WNSKR0hRKhWJvwYVsHdPSmZslS6BaX8cf38DggAN3p9k-OFHEVm7pB+oYBufL7yUiNhHtIoeAhgNV5AAxYQQAA2UjqAAphMZKCQYAwoH0wZCSMVEmUYvFEkD0jAcgBKEinHrCUzYXhwiCUZqoiFSNboACMr1uQA

最佳答案

TsetNumberField是一个黑盒子。没有人知道,即使是你,是否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

请记住,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

Playground

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


更新

Why adding a constraint such as T extends Record<string, number> isnot enough to make TS aware of the type of 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 会发生什么至 item[field]没有错误?调用此函数后,您希望每个值都是 BrandNumber但事实并非如此。

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

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

关于 typescript : Generic type "extract keys with value of type X" does not behave as expected,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70494434/

31 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com