且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

禁止显式类型参数或约束联合类型参数以获得更多类型安全

更新时间:2023-02-12 23:35:30

这可能比它的价值更麻烦;不幸的是,试图从类型系统获得此类保证所涉及的复杂程度很高,应该权衡人们手动指定太宽的类型参数的可能性.也许您可以只记录函数(/** 嘿,让编译器推断 T,谢谢 **/),而不是试图让编译器为您强制执行此操作.

This might be more trouble than it's worth; the level of complexity involved in trying to get such guarantees from the type system is unfortunately high, and should be weighed against the likelihood of people manually specifying too-wide type parameters. Perhaps you can just document the function (/** hey, let the compiler infer T, thanks **/) instead of trying to make the compiler enforce this for you.

首先,没有办法防止指定显式类型参数.即使你可以,它也可能对你没有多大帮助,因为类型推断仍然可能导致你试图避免的更广泛的类型:

First, there's no way to prevent explicit type parameters from being specified. Even if you could it might not help you much, since type inference could still result in the wider types you are trying to avoid:

const aOrB = Math.random() < 0.5 ? "a" : "b";
getValues(store, aOrB); // {a: string, b: string} again

您也无法在运行时访问类型参数,因为它们与静态类型系统的其余部分一样,是 在转换为 JavaScript 时擦除.

You also can't access type parameters at runtime because they, like the rest of the static type system, are erased upon transpilation to JavaScript.

环顾四周,我认为 TS 人员在尝试处理 计算属性键,它们也有类似的问题.提出的解决方案(但未在语言中实现,请参阅 microsoft/TypeScript#21030 和讨论的链接问题)似乎是一种叫做 Unionize 的东西,在其中你会得到 {a: string} |{b: string} 如果您不知道键是 "a" 还是 "b".

From looking around, I think TS folks have run into this when trying to deal with computed property keys, which have a similar problem. The solution proposed (but not implemented in the language, see microsoft/TypeScript#21030 and linked issues for discussion) seems to be something called Unionize in which you'd get {a: string} | {b: string} if you didn't know if the key is "a" or "b".

我想我可以想象使用像 Unionize 这样的类型与 编程交叉.这意味着您希望泛型参数不是键类型的联合,而是 rest tuple 关键类型:

I think I could imagine using a type like Unionize in concert with taking an programmatic intersection of these types for each key passed in. That implies you want your generic parameter not to be a union of key types, but a rest tuple of key types:

type UnionizeRecord<K, V> = K extends PropertyKey ? { [P in K]: V } : never;
type Expand<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
type UnionizeRecordTupleKeys<K extends PropertyKey[], V> = Expand<
    { [I in keyof K]: (x: UnionizeRecord<K[I], V>) => void }[number] extends
    ((x: infer R) => void) ? R : never
>;

UnionizeRecord 类型与 Record 类似,但产生一个类型联合,K代码>.Expand 类型仅有助于 IntelliSense 输出;它变成了一个像 {a: string} & 这样的交集.{bar: number} 转换为等效的单个对象类型,如 {a: string, bar: number}.而 UnionizeRecordTupleKeys 是丑陋的.它所做的是遍历 K 元组的每个索引 I 并生成一个 UnionizeRecord.然后将它们交叉在一起并展开它们.通过 distributive conditional 解释了所有工作方式的细节类型推断条件类型.

The UnionizeRecord<K,V> type is like Record<K, V> but produces a union of types, one for each key in K. The Expand<T> type just helps with IntelliSense output; it turns an intersection like {a: string} & {bar: number} into an equivalent single object type like {a: string, bar: number}. And UnionizeRecordTupleKeys<K, V> is, well, ugly. What it does is walks through each index I of the K tuple and produces a UnionizeRecord<K[I], V>. It then intersects them together and expands them. The specifics for how that all works are explained via distributive conditional types and inference in conditional types.

你的 getValues() 函数看起来像这样:

Your getValues() function then looks something like this:

function getValues<K extends (number extends K['length'] ? [string] : string[])>(
    store: Store, ...keys: [...K]
): UnionizeRecordTupleKeys<K, string> {
    const values = {} as any; // not worried about impl safety here

    keys.forEach((p) => {
        const value = store[p];
        if (value) {
            values[p] = value;
        } else {
            values[p] = 'no value';
        }
    });

    return values;
}

我不想让编译器理解它可以将事物分配给 UnionizeRecordTupleKeys 类型的值,当 K 是未指定的泛型时type 参数,所以我在实现中使用了 any.另一个问题是我约束K 类型是一个 tuple 可分配给 string[];我想防止像 Array<<"a" 这样的非元组|b">.防止这种情况确实必要,因为输出类型只会比您喜欢的更宽,但我想防止有人反对有人手动指定一些奇怪的东西.您可以使用类似的技巧来防止其他特定的事情.

I'm not bothering trying to make the compiler understand that it can assign things to a value of type UnionizeRecordTupleKeys<K, string> when K is an unspecified generic type parameter, so I used any inside the implementation. The other wrinkle is that I constrained the K type to be a tuple assignable to string[]; I want to prevent a non-tuple like Array<"a" | "b">. It's not really necessary to prevent this, since the output type would just be wider than you like, but I wanted to forestall an objection about someone manually specifying something weird. You can use similar tricks to prevent other specific things.

让我们看看它是如何工作的.这仍然表现得如你所愿:

Let's see how it works. This still behaves how you want:

const values1 = getValues(store, 'a', 'b');
/*const values1: {
    a: string;
    b: string;
} */

如果你手动指定一个类型参数,它需要是一个元组:

If you manually specify a type parameter, it needs to be a tuple:

const values2 = getValues<["a", "b"]>(store, 'a', 'b');
/* const values2: {
    a: string;
    b: string;
} */

如果你指定了错误的元组,你会得到一个错误:

If you specify the wrong tuple, you get an error:

const values3 = getValues<["a", "b", "c"]>(store, 'a', 'b'); // error!
// -----------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Expected 4 arguments, but got 3.

如果你为元组元素之一指定或传入联合,你会得到对象类型的联合:

If you specify or pass-in a union for one of the tuple elements, you get the union of object types out:

const aOrB = Math.random() < 0.5 ? "a" : "b";
const values4 = getValues(store, aOrB); 
/* const values4: {
    a: string;
} | {
    b: string;
} */

这可以成为很多东西的有趣结合:

which can become a fun union of lots of things:

const cOrD = Math.random() < 0.5 ? "c" : "d";
const values5 = getValues(store, aOrB, cOrD); 
/* const values5: {
    a: string;
    c: string;
} | {
    a: string;
    d: string;
} | {
    b: string;
    c: string;
} | {
    b: string;
    d: string;
} */


这样一切正常!不过,可能有很多边缘情况我没有考虑过……即使它有效,它也很笨拙和复杂.再说一次,我可能只是建议记录您的原始函数,以便调用者了解编译器的局限性.但是很高兴看到您可以接近正确"的位置.输出类型!


So that all works! There are probably plenty of edge cases I haven't considered, though... and even if it works it's clunky and complicated. So again, I'd probably just recommend documenting your original function so that callers are aware of the limitations of the compiler. But it's neat to see how close you can get to the "correct" output type!

游乐场链接到代码