禁止显式类型参数或约束联合类型参数以获得更多类型安全 [英] Forbid explicit type parameters or constrain union type parameters for more type safety

查看:26
本文介绍了禁止显式类型参数或约束联合类型参数以获得更多类型安全的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

getValues() 函数在没有显式类型参数的情况下被调用时,以下 Typescript 代码工作正常.但是,当它使用显式类型参数调用时,可能会导致错误(请参阅调用 getValues<'a' | 'b' | 'c' >(store, 'a', 'b') 下面).

The following Typescript code works fine, when the getValues() function is invoked without explicit type parameters. However when it is called with explicit type parameters, it can lead to errors (see invocation getValues<'a' | 'b' | 'c' >(store, 'a', 'b') below).

type Store = Record<string, string>;

function getValues<T extends string>(
    store: Store, ...keys: T[]
): {[P in T]: string} {
    const values = {} as {[P in T]: string};

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

    return values;
}


// Usage / requirements:    

const store: Store = {}; // assume it is filled with arbitrary values


// in the following line values1 is inferred to be of type
// {a: string, b: string}, which is the required behavior
const values1 = getValues(store, 'a', 'b');


// the following line does not compile, which is the required behavior
// Argument of type 'b' is not assignable to parameter of type 'a' | 'c'
const values2 = getValues<'a' | 'c'>(store, 'a', 'b');


// in the following line values3 is inferred to be of type
// {a: string, b: string, c: string}, which is NOT the required behavior
// the required behavior would be to forbid this situation, similar to
// how values2 declaration was forbidden by the compiler.
const values3 = getValues<'a' | 'b' | 'c' >(store, 'a', 'b');

// now values3.c can be accessed
// values3.c cannot have a value and will cause an error when used

有没有办法阻止调用者提供显式类型参数或任何其他方式来使此代码更加类型安全,而无需使 getValues 函数的返回类型部分化或使其返回类型的属性可选.

Is there a way to prevent the caller from providing explicit type parameters or any other way to make this code more type-safe without making the return type of the getValues function partial or making its return type's properties optional.

我想到了以下想法,我不知道它们是否或如何在 Typescript 中实现:

The following ideas come to my mind, for which I don't know whether or how they are possible in Typescript:

想法 1:防止提供显式类型参数

Idea 1: prevent explicit type parameters to be provided

想法 2:移除 getValues 的值参数并在运行时访问提供的类型参数

Idea 2: remove getValues' value arguments and access the provided type arguments at runtime

想法 3:强制所有提供的类型参数也用作值参数.即上面的values3常量的声明会导致编译错误.

Idea 3: enforce all provided type arguments to be used as value arguments also. That is, the declaration of values3 constant above will lead to a compile error.

我知道问题是由不安全的类型断言as {[P in T]: string}引起的.我正在寻找一种方法来使这种类型安全.

I am aware that the problem is caused by the unsafe type assertion as {[P in T]: string}. I am looking for a way to make this type-safe.

推荐答案

这可能比它的价值更麻烦;不幸的是,试图从类型系统获得此类保证所涉及的复杂程度很高,应该权衡人们手动指定太宽的类型参数的可能性.也许您可以只记录函数(/** 嘿,让编译器推断 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!

游乐场链接到代码

这篇关于禁止显式类型参数或约束联合类型参数以获得更多类型安全的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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