【问题标题】:Constrain 'pickable' attributes after picks in pick function (TypeScript)在挑选功能(TypeScript)中挑选后约束\'pickable\'属性
【发布时间】:2022-10-05 04:13:45
【问题描述】:

我在我的代码库中创建了以下 util 函数:

const pick = <T extends object, P extends keyof T, R = Pick<T,P>>(
obj: T,
keys: P[]
): R => {
if (!obj) return {} as R
return keys.reduce((acc, key) => {
   return {...acc, [key]:obj[key] };
}, {} as R)
};

该函数工作正常,并且 TS 推断出正确的返回类型。唯一的问题是 keys 参数,我想根据之前选择的键来限制它。

例子:

const obj = {name: 'John Doe', age: '33', city: 'NYC'} 

// When typing the keys in the keys array param, it infers the keys correctly
const a = pick(obj, ['name', 'age']) 

// BUT, this is also possible, and TS doesnt complain
const b = pick(obj, ['name', 'age', 'age']) 

// ALSO, when I have already entered for example 'name', I want intellisense only to show 'age' and 'city' as possible options, currently it still shows all keys.

我尝试了很多东西(甚至对函数进行了柯里化),但没有成功,事实证明这是一个艰难的 TS 难题。我希望我能得到一些帮助!

【问题讨论】:

  • 您的 R 类型参数是多余的,出于您的问题的目的,可能应该用 Pick&lt;T, P&gt; 替换......即使您想使用通用默认值作为速记,它也可能超出了所问问题的范围。
  • 你能解释一下你为什么关心这个吗? pick(obj, ["name", "age", "age"]) 有问题吗?输出是否以某种方式出乎意料?
  • 请注意,您可以获得您想要的行为,如this playground example 所示,但对于看似可疑的好处来说,这非常复杂。如果你愿意,我可以把这个解决方案写成答案......除非有什么对你不起作用的东西。让我知道。
  • @jcalz 感谢您的回答,它肯定更接近我想要的。唯一剩下的是智能感知,你可以在这里看到它:LINK,名字已经被选中,所以它不应该是一个选项。所以对于下一个可选择的键,我只想看到:[年龄,城市]。我知道它矫枉过正,输出并不意外,但我喜欢智能感知是对仍然可能的事情的反映,而不是所有可能性。如果那有意义的话。我很想写下你的解决方案。我非常感谢您的努力。这让我很头疼
  • 我认为不可能让 IntelliSense 自动完成提示执行您想要的操作,因为据我所知,它从约束 K extends Array&lt;keyof T&gt; 获取该列表...约束必须是 K extends ExcludeArray&lt;keyof T, K&gt; 才能给您IntelliSense,但该约束是非法循环的。您仍然希望将答案写下来还是 IntelliSense 提示更重要?

标签: javascript typescript function


【解决方案1】:

TypeScript 本身并不支持没有重复元素的“唯一”数组的概念。所以没有简单的方法来写keys: UniqueArray&lt;P&gt;

相反,您可以尝试构建一个复杂的genericconditional 类型,其作用类似于您为keys 传入的tuple type 的约束,其中每个元素类型excludes 所有先前的元素类型。因此,如果您传入一个K extends Array&lt;keyof T&gt; 类型的数组,编译器将确保K 可分配给ExcludeArray&lt;keyof T, K&gt;,如果没有则抱怨。

它可能看起来像这样:

type ExcludeArray<T, U extends any[], A extends any[] = []> =
    number extends U['length'] ? [...T[]] : (
        U extends [infer F, ...infer R] ?
        ExcludeArray<Exclude<T, F>, R, [...A, F extends T ? F : T]> : A
    );

function pick<T extends object, K extends Array<keyof T>>(
    obj: T,
    keys: [...K] extends ExcludeArray<keyof T, K> ? K : ExcludeArray<keyof T, K>
): Pick<T, K[number]>;
function pick(obj: any, keys: PropertyKey[]) {
    if (!obj) return {}
    return keys.reduce((acc, key) => {
        return { ...acc, [key]: obj[key] };
    }, {})
};

ExcludeArray&lt;T, U&gt; 类型接受 T 类型和元组类型 U 并遍历 U 构建它的一个版本,其中每个元素都排除所有先前的元素:

type Example1 = ExcludeArray<1 | 2 | 3 | 4, [1, 2, 3, 4]> 
// type Example1 = [1, 2, 3, 4]
type Example2 = ExcludeArray<1 | 2 | 3 | 4, [1, 2, 2, 4]> 
// type Example2 = [1, 2, 3 | 4, 4]

所以Example1和传入的U是一样的,因为没有重复。但Example2 显示第三个元素已从2(重复)更改为3 | 4(当时允许的值)。

请注意,在一个完美的世界中,我们可以这样写pick()

function pick<T extends object, K extends ExcludeArray<keyof T, K>>(
  obj: T, keys: K
): Pick<T, K[number]> {...}

但是约束K extends ExcludeArray&lt;keyof T, K&gt;&gt; 是非法循环的。相反,我们需要跳过K extends Array&lt;keyof T&gt; 在声明时的圈套,然后我们说服编译器将K 视为在使用站点受到如此限制,因此keys: [...K] extends ExcludeArray&lt;keyof T, K&gt; ? K : ExcludeArray&lt;keyof T, K&gt;


无论如何,让我们测试一下:

const obj = { name: 'John Doe', age: '33', city: 'NYC' }

const a = pick(obj, ['name', 'age']); // okay
const b = pick(obj, ['name', 'age', 'age']); // error!  
// -------------------------------> ~~~~~
// Type '"age"' is not assignable to type '"city"'
const c = pick(obj, ["name", "age", "ss"])
// -------------------------------> ~~~~
// Type '"ss"' is not assignable to type '"name" | "age" | "city"'

这表现得如愿。


不幸的是,正如您所注意到的,您没有从 IntelliSense 获得自动完成提示行为。当您正在调用pick() 时,提示的值将是完整的键集,甚至是您已经使用过的键:

const d = pick(obj, ["age", "?"]) 
// ------------------------> ^^ <-- prompted with "age", "city", "name"

所以你可以选择重复,然后编译器会抱怨。

我对此无能为力。 IntelliSense 仅使用约束 Array&lt;keyof T&gt; 进行提示,它不会尝试评估 ExcludeArray&lt;keyof T, K&gt;,也不能轻松地这样做,因为您的中期开发字符串文字不太可能是正确的值之一,因此推理失败反正。这基本上超出了编译器的能力。


所以上面我提出了一个复杂的解决方案,它的行为主要是你想要的方式,但没有很好的 IntelliSense 支持。您可能需要考虑替代方案......也就是说,只需接受 Array&lt;keyof T&gt; 并可能重复。毕竟,来自pick() 的值和类型仍然是正确的。 ["name", "age", "age"] 的问题是最后一个 "age"不必要;它不是有害以任何方式。由于允许重复的后果是如此轻微,我会说在这种情况下治愈比疾病更糟糕。但这当然取决于你。

Playground link to code

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-07
    • 1970-01-01
    • 2021-04-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多