类型“可分配给类型‘T’的约束,但‘T’可以使用约束‘RGT’的不同子类型进行实例化

Jak*_*one 14 typescript

我在 TypeScript 中遇到了这个错误消息。我查看了以下答案,但我根本不明白这些解决方案。

这是我的代码的精简版本,其中包含错误:

interface R {
    field: string;
    operator: string;
    value: any;
}
interface RG {
    combinator: 'and'|'or';
    rules: (R|RG)[];
}
interface RGIC {
    rules: (RGIC|R|string)[];
}
type RGT = RG | RGIC;

const f = <T extends RGT>(r: T): T => {
    if ('combinator' in r) {
        return { combinator: 'and', rules: [] }; // <-- TS error here
    }
    return { rules: [] }; // <-- TS error here
}
Run Code Online (Sandbox Code Playgroud)

我想表达的是,如果r是类型RG,那么函数f将返回一个也是类型的对象RG。如果r是 type RGIC,则函数f将返回一个也是 type 的对象RGIC。但这个错误让我很困惑。

TS 游乐场链接

jca*_*alz 40

您遇到的问题是形式的通用约束T extends RGT并不意味着“T必须恰好是RGRGIC”。它所暗示的是T与兼容RGT这意味着它是 的子类型可分配给 RGT。有一些子类型RGTRG或更具体RGIC,例如:

interface Whaa {
    combinator: 'or',
    rules: R[],
    brainCellCount: number;
}

const whaa: Whaa = {
    combinator: 'or',
    rules: [],
    brainCellCount: 86e9
}
Run Code Online (Sandbox Code Playgroud)

Whaa接口可分配给RG;每个Whaa都是有效的RG。但是 aWhaa有一个combinatorwhich 必须是"or",并且它还有一个附加brainCellCount属性 type number

如果你的函数f有这样的调用签名:

declare const f: <T extends RGT>(r: T) => T;
Run Code Online (Sandbox Code Playgroud)

你是说 的返回类型f将与传入的类型完全相同r。因此以下调用Whaa也将产生类型的结果:

const hmm = f(whaa);
// const hmm: Whaa
Run Code Online (Sandbox Code Playgroud)

如果是这样,那么就可以了:

hmm.brainCellCount.toFixed(2); // no compiler error
Run Code Online (Sandbox Code Playgroud)

但你的实现并不f好:

const f = <T extends RGT>(r: T): T => {
    if ('combinator' in r) {
        return { combinator: 'and', rules: [] }; // error
    }
    return { rules: [] }; // error
}

hmm.brainCellCount.toFixed(2); // no compiler error
// BUT AT RUNTIME:  brainCellCount is undefined !!
Run Code Online (Sandbox Code Playgroud)

现在这个错误应该是有道理的。您返回一个类似于 的值{combinator: "and", rules: []}声称它与T的类型相同r。但编译器表示,这样的值虽然可分配给RGT,但可能无法分配给T


有不同的方法可以处理这个问题。由于您实际上只想说如果输入是则RG输出​​也是,并且如果输入是RGIC则输出​​也是,那么最简单的方法可能是使用两个调用签名创建f一个重载函数:

// call signatures
function f(r: RG): RG;
function f(r: RGIC): RGIC;

// implementation
function f(r: RGT): RGT {
    if ('combinator' in r) {
        return { combinator: 'and', rules: [] };
    }
    return { rules: [] };
}
Run Code Online (Sandbox Code Playgroud)

这编译没有错误。尽管您应该小心:它并不能真正保证类型安全;编译器不会捕获错误,if ('cobminator' in r)因为重载实现的检查是松散的。只要每个return语句适用于某个调用签名,编译器就会很高兴,即使您得到了错误的签名。因此,只需仔细检查您的实现是否适用于每个调用签名。确实如此,所以这很好。

让我们看看当我们调用它时它是如何工作的:

const hmm = f(whaa);
// const hmm: RG
hmm.brainCellCount //<-- now this is a compiler error
Run Code Online (Sandbox Code Playgroud)

看起来不错。现在编译器接受它whaa是 anRG并且输出类型也是 an RG。如果您尝试访问brainCellCount的属性hmm,编译器现在会抱怨 上没有这样的属性RG


另一种类似的处理方法是继续使用泛型函数,但返回类型是条件类型,如下所示:

const f = <T extends RGT>(r: T): T extends RG ? RG : RGIC => {
    if ('combinator' in r) {
        return { combinator: 'and', rules: [] } as any;
    }
    return { rules: [] } as any;
}
Run Code Online (Sandbox Code Playgroud)

该返回类型T extends RG ? RG : RGIC捕获了您将返回类型扩大到RG(如果输入是 的某个子类型)的意图RG,否则扩大到RGIC。请注意,在这两个return语句中,我必须使用类型断言any我本来可以这样做as T extends RG ? RG : RGIC)来使其编译。这是因为编译器实际上无法理解何时根据未指定的泛型类型参数将某些特定值分配给条件类型。microsoft/TypeScript#33912有一个开放的功能请求,要求那里有更好的东西,但我不知道它何时或是否会改进。目前,类型断言或重载是可行的方法。

不管怎样,你可以看到它的行为是一样的:

const hmm = f(whaa);
// const hmm: RG
hmm.brainCellCount // error
Run Code Online (Sandbox Code Playgroud)

当您调用f(whaa)编译器时,推断是TWhaa然后计算结果T extends RG ? RG: RGICRGhmm所需的类型也是如此RG,但未知其是否具有brainCellCount属性。您会在预期的地方得到编译器错误。


Playground 代码链接

  • 这个答案最终帮助我理解了这个我已经见过很多次但从未理解过的错误。我只是从未意识到您可以扩展接口,同时使现有的键类型更具体(因为我从来不想这样做)。非常感谢您详细而耐心的解释! (2认同)