打字稿省略助手不允许覆盖属性

时间:2020-06-23 13:46:08

标签: typescript tsc

interface ContextValue {
    [K: string]: {
        value: any
        error: boolean
        errorMessage: string
    }
}

export interface GatheringExpChildMachineBaseContext<TYPE> extends Omit<ContextValue, 'id' | 'originalData' | 'order'> {
    id?: string
    originalData: TYPE
    order: number
}

我收到错误消息TS 2411 Property 'id' of type 'string' is not assignable to string index type '{ value: any; error: boolean; errorMessage: string; }'.以及与originalDataorder等效的消息。

这似乎是不正确的,因为忽略应该抓住ContextValue界面的键(可以是任何键),并忽略我提供的'id' | 'originalData' | 'order'的并集类型,并允许我覆盖它们。

1 个答案:

答案 0 :(得分:2)

TypeScript当前不支持减法/ negated types,这就是您需要说的“ stringnot ("id" | "originalData" | "order")”的意思。通过使用键上的Exclude<T, U> utility type来实现OmitExclude<T, U>过滤T的子类型U的任何并集成员。由于string不是联合,因此筛选器将整体上作用于string。而且,由于string不是字符串文字的有限并集的子类型(它是超类型,而不是子类型),因此Exclude<string, "id" | "originalData" | "order">的值仅为string。从某种意义上讲,当前的TypeScript版本认为“ {string减去("id" | "originalData" | "order")等于string”。

您正在寻找的特定功能(可以在其中对索引签名进行例外处理)是GitHub中一个未解决问题的主题:microsoft/TypeScript#17867。如果您想在TypeScript中实现此功能,则可能要解决该问题并给它一个a。它也标记为“等待更多反馈”,因此,如果您认为它有一个引人注目的用例(尚未涵盖),则可能需要发表评论以详细说明它。

目前只有多种解决方法,每种解决方法都有一个相关的痛点。


您可以使用交集来避免该错误,从而可以轻松使用该类型的现有值:

interface Extension<T> { id?: string, originalData: T, order: number }
type GECMBCIntersection<T> = ContextValue & Extension<T>

可以轻松使用该类型的现有值

declare const i: GECMBCIntersection<number>;
i.id?.toUpperCase(); // okay
i.originalData.toFixed(); // okay
i.order.toFixed(); // okay
i.somethingElse.errorMessage.toUpperCase(); // okay

但是,出于相同的不兼容索引签名原因,要验证值是否属于该类型并不容易:

const iBad: GECMBCIntersection<number> = {
    originalData: 1,
    order: 2
} // error! originalData is not assignable to index signature

您可以在实际键中将其表示为generic constraint,而不是带有索引签名的具体类型:

type GECMBCConstraint<K extends PropertyKey, T> =
    Record<Exclude<K, keyof Extension<any>>, ContextValue[string]> & Extension<T>;

但是随后您需要在每个地方都携带一个额外的泛型类型参数,并使用辅助函数创建没有冗余键名的实例:

const asGECMBCConstraint = <G extends GECMBCConstraint<keyof G, any>>(g: G) => g;
const cGood = asGECMBCConstraint({
    originalData: 1,
    order: 2,
    somethingElse: {
        value: "blork",
        error: true,
        errorMessage: "blork!!",
    }
})

即使这样,当您要访问erstwhile-index-signature属性时,也很难使用实际值:

function cBad<G extends GECMBCConstraint<keyof G, any>>(g: G) {
    if ("somethingElse" in g) {
        g.somethingElse; // error
    }
    const asserted = g as GECMBCIntersection<any>;
    if ("somethingElse" in asserted) {
        asserted.somethingElse.errorMessage.toUpperCase() // okay
    }

}

现在,我通常建议重构代码,以免将索引签名与其他属性混在一起。如果可以,请重写您的类型,使其具有ContextValue,而不是 ContextValue。 (这也称为composition over inheritance)。然后,您将获得更简单的界面:

interface Easier<T> extends Extension<T> {
    contextValues: ContextValue;
}

这对于编译器来说很容易推理:

const easy: Easier<number> = {
    originalData: 1,
    order: 2,
    contextValues: {
        somethingElse: {
            value: "blork",
            error: true,
            errorMessage: "blork!!",
        }
    }
};

好的,希望能有所帮助;祝你好运!

Playground link to code