【问题标题】:Constrain one Typescript generic parameter based on properties of another?根据另一个的属性约束一个 Typescript 通用参数?
【发布时间】:2021-02-03 12:23:32
【问题描述】:

我正在尝试编写一个函数,该函数接受一个对象和一个(字符串)键,然后对该对象的属性进行操作。这很简单:

function f<T extends any, K extends keyof T>(obj: T, key: K) {
  const prop = obj[key]; // prop is typed as T[K]
}

我想在编译时根据T[K] 的类型限制传递给调用的键。我试过这个:

function f<T extends any, K extends keyof T>(obj: T, key: T[K] extends number ? K : never) {
  obj[key] = 5; // error, "is not assignable to" etc
}

prop 被输入为 T[T[K] extends number ? K : never],在我看来它应该折叠为 number,但它没有。

我的目标是确保在函数内部将obj[key] 键入为number,并且还将诸如f({a: true}, "a") 之类的调用标记为错误。这可能吗?我想我可能需要将约束从函数参数声明移动到泛型参数声明,但我无法弄清楚语法。


ETA 再次:Playground example -- 更新以尝试@reactgular 在评论中建议的方法:

type AssignableKeys<T, ValueType> = {
  [Key in keyof T]-?: ValueType extends T[Key] | undefined ? Key : never
}[keyof T];

type PickAssignable<T, ValueType> = Pick<T, AssignableKeys<T, ValueType>>;

type OnlyAssignable<T, ValueType> = {
  [Key in AssignableKeys<T, ValueType>]: ValueType
};

interface Foo {
  a: number;
  b: string;
  nine: 9;
  whatevs: any;
}

type FooNumberKeys = AssignableKeys<Foo, number>; // "a" | "whatevs"
type CanAssignNumber = PickAssignable<Foo, number>; //  { a: number; whatevs: any; }
type DefinitelyJustNumbers = OnlyAssignable<Foo, number>; //  { a: number; whatevs: number; }

function f1<T>(obj: OnlyAssignable<T, number>, key: keyof OnlyAssignable<T, number>) {
  obj[key] = Math.random(); // Assignment is typed correctly, good
}

function f2<T extends object, K extends keyof PickAssignable<T, number>>(obj: T, key: K) {
  obj[key] = Math.random(); // Uh oh, Type 'number' is not assignable to type 'T[K]'.(2322)
}

declare const foo: Foo;
f1(foo, "a"); // Allowed, good
f1(foo, "whatevs"); // Allowed, good
f1(foo, "nine"); // Uh oh, should error, but doesn't!
f1(foo, "b"); // Error, good

f2(foo, "a"); // Allowed, good
f2(foo, "whatevs"); // Allowed, good
f2(foo, "nine"); // Error, good
f2(foo, "b"); // Error, good

在 Playground 中,DefinitelyJustNumbers 显示{a: number; whatevs: number} 的工具提示——我可以将number 分配给的任何内容都明确键入为number。这修复了函数体内的赋值,但未能检测到 nine 只是数字的子集,因此不应被允许。

CanAssignNumber 显示{a: number; whatevs: any} 的工具提示,正确排除了nine,因为它不能分配给number。这看起来不错,但仍然不能修复函数 f2 内的分配。

【问题讨论】:

标签: typescript generics conditional-types


【解决方案1】:

您应该从产生number 类型值的键扩展。

export type PickByValue<T, ValueType> = Pick<
  T,
  { [Key in keyof T]-?: T[Key] extends ValueType ? Key : never }[keyof T]
>;

function f<T extends object, K extends keyof PickByValue<T, number>>(obj: T, key: K) : T[K] {
  return obj[key]
}

编辑:您尝试做的事情在 TS AFAIK 中是不可能的,有时这是有充分理由的。假设您有以下代码:

function f<T extends object, K extends keyof PickByValue<T, number>>(obj: T, key: K) {
    obj[key] = 5; // Type 'number' is not assignable to type 'T[K]'.
} 

const obj = {a: 9} as const;

f(obj, "a")

例如在上述场景中,属性a 的值是一个数字,但它不是number 类型,而是9 类型。打字稿无法事先知道这一点。在其他情况下,我唯一想到的就是使用Type Assertions

【讨论】:

  • 这是表达我想要做的事情的有用方式,谢谢!不幸的是,它doesn't actually work
  • 我更详细地更新了这个问题。为时已晚,无法编辑上述评论,抱歉。
  • Per Titians 的回答如下,我想要的是T[K] 类型的“上限约束”(上限?),因此约束类型(示例中为number)是始终可分配给T[K]。我的问题中更新的示例将捕获您的反例并将其标记为无效。 (不过,我仍然无法获得在函数体中工作的可分配性。)
  • @Coderer 我真的想不出一种方法来实现您想要实现的目标。以目前的 TS 力量,我认为你不能做得比这更好。最好首先质疑为什么需要重新分配输入,通常有更好的方法。
【解决方案2】:

只需使用类型断言,您关心的应该是调用站点,它输入正确,并在应有的地方给您错误。如果您想在函数内分配特定值,则无法真正正确键入实现。

您可以将T[K]的结果扩展为数字,例如但将约束添加到Record&lt;K, number&gt;T,但我们仍然无法为obj[key]分配具体值

type KeyOfType<T, ValueType> = 
  { [Key in keyof T]-?: T[Key] extends ValueType | undefined ? Key : never }[keyof T]

function f<T extends Record<K, number>, K extends KeyOfType<T, number>>(obj: T, key: K, value: T[K]) {
    let r = obj[key]; 
    r.toExponential(); // seems numberish, but it `T[K]` which does extend number, but might not be number
    obj[key] = obj[key] // T[K] is assignable to T[K]
    obj[key] = value; // even if it is a parameter 
    obj[key] = 5; // still an error
}

declare const foo: Foo;
f(foo, "a", 1); // Allowed, good
f(foo, "b", 2); // Error, good

const other: { 
  a: 1
} = {
  a: 1
}
f(other, "a", 1) // this will break the type of other because of obj[key] = 5

Playground Link

之所以如此,是因为最后一个例子f(other, "a", 1)。这里other 中的a 具有1 类型,它确实扩展了number,所以f(other, "a", 1) 是对f 的有效调用,但在内部我们要分配other[key] = 5。这会破坏other 的类型。这里的问题是,没有办法指定T[K] 的上限为number,只是下限。

【讨论】:

  • 一:输入类型T需要被允许有不是number的属性,所以Record没有帮助。二:我想在键K 上设置一个“上限约束”,这样某些类型(我们称之为V)可以分配给T[K]。我不明白为什么不能表达这种约束。我用一个更好(但仍然失败)尝试的新 Playground 示例更新了问题。
  • @Coderer 类型系统不支持上限。当你表达一个约束比如T extends numberT可以是数字,也可以是number的任何子类型,可以是任何数字字面量类型比如12等。你可以得到编译器理解T[K] extends number,但这仅意味着T[K]必须是number的子类型,不一定是number。所以分配obj[key] = 1 不正确,因为obj[key] 可能是number12 类型,对于其中之一,分配将是不正确的
  • 不过,在上面的重写中,我有number extends T 或类似的东西。当然,这仅在映射类型的“r-value”中才有可能,而不是在通用约束中,但我最新问题更新中的映射类型确实与我想要的键匹配,所以我希望它会渗透.听起来我仍然需要断言,但是在我刚刚进行的更改之后,我无法想出一个反例来说明它为什么是错误/危险的。
猜你喜欢
  • 2014-04-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多