是否可以将泛型类型限制为仅允许已知属性?

Jes*_*end 3 typescript

如果向函数提供的对象具有太多属性,则会出现错误:

type Options = {
    str: "a" | "b",
}

function foo(a: Options) {
    return a.str;
}

const resultA = foo({
    str: "a",
    extraOption: "errors as expected",
//  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^------ Object literal may only specify known properties.
});
Run Code Online (Sandbox Code Playgroud)

这个不错,我想要这个 但因为我知道我将根据其输入返回什么类型,所以我想让该函数变得通用,如下所示:

function bar<T extends Options>(a: T) {
    return a.str as T["str"];
}
Run Code Online (Sandbox Code Playgroud)

但现在输入上允许使用额外的属性。

const resultB = bar({
    str: "a",
    extraOption: "no error!?",
});
Run Code Online (Sandbox Code Playgroud)

有什么办法可以限制这个吗?

游乐场链接

jca*_*alz 6

TypeScript 中的对象类型不是“密封的”;允许有多余的属性。这使得各种好东西成为可能,例如接口和类扩展,并且只是 TypeScript结构类型系统的一部分。多余的属性总是有可能潜入的,因此,如果函数参数具有此类属性,您应该确保您的foo()bar()实现不会做任何可怕的事情。如果您通过类似method之类的Object.keys()方法迭代属性,您应该假设结果只是已知的键。(这就是为什么Object.keys(obj)返回string[]

当然,扔掉多余的财产通常表明存在问题。如果将对象文字直接传递给函数,那么该文字上的任何额外属性很可能会被完全忽略和遗忘。这可能表明存在错误,因此 对象文字会检查多余的属性

function foo(a: Options) {
    return a.str;
}

const resultA = foo({
    str: "a",
    extraOption: "errors as expected", // error
});
Run Code Online (Sandbox Code Playgroud)

为什么这种情况会因为类似的事情而消失

function bar<T extends Options>(a: T) {
    return a.str as T["str"];
}
const resultB = bar({
    str: "a",
    extraOption: "no error!?",
});
Run Code Online (Sandbox Code Playgroud)

是因为泛型函数确实有可能跟踪这些额外的属性:

function barKT<T extends Options>(a: T) {
    return { ...a, andMore: "hello" }
}
const resultKT = barKT({ str: "a", num: Math.PI });
console.log(resultKT.num.toFixed(2)) // "3.14"
Run Code Online (Sandbox Code Playgroud)

但在您的版本中,bar()您只归还str财产,因此多余的财产确实会丢失。因此,您希望在多余的属性上出现错误,但您没有得到错误。


但这提出了一个问题:为什么是bar()通用的?如果您不想允许多余的属性并且您只关心一个str属性,那么就没有明显的动机T extends Options。如果您只想尽可能缩小 的类型T["str"],那么实际上您只希望输入的该部分是通用的:

function barC<S extends Options["str"]>(a: { str: S }) {
    return a.str;
}

const resultC = barC({
    str: "a",
    extraOption: "error", // error here
});
Run Code Online (Sandbox Code Playgroud)

但是,如果由于某种原因您确实需要T extends Options,您可以通过将函数输入设置为映射类型来阻止多余的属性,其中多余的属性将其属性值类型映射到typenever。由于没有 type 的值never,传入的对象字面量无法满足该要求,并且会生成错误:

function barD<T extends Options>(a: 
  { [K in keyof T]: K extends keyof Options ? T[K] : never }
) {
    return a.str
}

const resultD = barD({
    str: "a",
    extraOption: "error", // error here
});
Run Code Online (Sandbox Code Playgroud)

万岁!


同样,虽然这会阻止过多的财产,但并不能绝对阻止它们。结构子类型要求您可以将 a 扩大{str: "a" | "b", extraOption: string}{str: "a" | "b"}

const someValue = {
    str: "a",
    extraOption: "error",
} as const;

const someOptions: Options = someValue; // okay

barC(someOptions); // no error
barD(someOptions); // no error
Run Code Online (Sandbox Code Playgroud)

同样,您应该确保您的实现不会假设多余的属性是不可能的。

Playground 代码链接