禁止显式类型参数或约束联合类型参数以提高类型安全性

Fat*_*kun 2 typescript typescript-generics

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

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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

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

jca*_*alz 5

这可能带来的麻烦大于其价值;不幸的是,尝试从类型系统获得此类保证所涉及的复杂性很高,并且应该权衡人们手动指定太宽的类型参数的可能性。也许您可以只记录函数 ( /** hey, let the compiler infer T, thanks **/),而不是尝试让编译器为您强制执行此操作。


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

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

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


环顾四周,我认为 TS 人员在尝试处理计算属性键时也遇到过这个问题,它们也有类似的问题。提出的解决方案(但未以该语言实现,请参阅microsoft/TypeScript#21030和链接问题进行讨论)似乎是一种称为“Unionize如果您不知道{a: string} | {b: string}键是否为"a"or ”的解决方案"b"


我想我可以想象使用类似的类型Unionize,并为传入的每个键采用这些类型的编程交集。这意味着您希望泛型参数不是键类型的联合,而是键类型的其余元组:

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
>;
Run Code Online (Sandbox Code Playgroud)

UnionizeRecord<K,V>类型类似于Record<K, V>,但会生成类型的联合,每个类型对应 中的每个键K。该Expand<T>类型仅有助于 IntelliSense 输出;它将交集转变{a: string} & {bar: number}为等效的单个对象类型,如{a: string, bar: number}。而且UnionizeRecordTupleKeys<K, V>,嗯,很丑。它的作用是遍历元组I的每个索引K并生成一个UnionizeRecord<K[I], V>. 然后将它们相交并展开。所有工作原理的具体细节都是通过分布式条件类型条件类型中的推理来解释的。

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

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;
}
Run Code Online (Sandbox Code Playgroud)

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


让我们看看它是如何工作的。这仍然是你想要的行为:

const values1 = getValues(store, 'a', 'b');
/*const values1: {
    a: string;
    b: string;
} */
Run Code Online (Sandbox Code Playgroud)

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

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

如果指定了错误的元组,则会出现错误:

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

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

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

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

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;
} */
Run Code Online (Sandbox Code Playgroud)

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

Playground 代码链接