簡體   English   中英

如何鍵入具有相關約束的通用接口的記錄?

[英]How to type a record of generic interfaces with related constraints?

我正在使用解析器工具包 (Chevrotain) 編寫查詢語言,並希望允許用戶擴展其功能。 我擁有執行此操作所需的所有部分,但正在為這種擴展行為定義類型而苦苦掙扎。 我希望能夠輸入配置 object,這樣使用 Typescript 的用戶將有方便的 IDE 幫助斷言他們的輸入是正確的; 這似乎是可能的(或非常接近可能),所以我一直在嘗試編寫類型(而不是在運行時斷言)。

一些配置的(簡單的)示例:

ops: {
  equal: {
    lhs: {
      type: 'string',
      from: v => String(v),
    },
    rhs: {
      type: 'number',
      from: v => v.toString(),
    },
    compare: (lhs, rhs) => lhs === rhs,
  }
  equal: { /*...*/ }
}

我希望以下內容為真:

  1. from的參數類型與type屬性的字符串文字值相關。 我已經設法通過幾種方式實現了這一點,其中最干凈的是一種簡單的類型,例如:
type ArgTypes = {
  string: string,
  number: number,
  ref: any, // the strings don't have to be typescript types, and the types could be more complex
}
  1. lhsrhs字段可以接收彼此不同的類型,並產生彼此不同的類型。

  2. compare function 將lhsrhs屬性的output作為輸入,並返回 boolean。

我已經能夠在單個運算符 ( equal ) 級別鍵入內容,但無法將其擴展到運算符的對象包中。 這是一個 Playground 鏈接,我在其中嘗試使用 generics 和子類型一次構建它: attempt N 在這次嘗試中,一旦到達對象映射位,我似乎就無法堅持使用窄類型; 可能無法在第 105 行為Ops提供有效的類型簽名?

另一個(靈感來自Preventing object literals type widenning when passed as argument in TypeScript )我試圖一次完成所有操作,只需為每件該死的事情添加類型 arguments : 嘗試 N+1 這幾乎可以工作,但是當您取消注釋類型簽名中的“比較”行時,(以前工作的)窄類型變得通用。 (例如文字"number"變成string

有可能這樣做還是我應該放棄? 如果是這樣,如何?

這里的潛在問題是 TypeScript 在同時推斷泛型類型參數和上下文類型function 參數方面的能力有限。 推理算法是一組合理的啟發式算法,適用於許多常見用例,但它並不是一個完全統一的算法,可以保證將正確的類型分配給所有泛型類型 arguments 和所有未注釋的值,如建議的那樣(但尚未或可能曾經實現過)在microsoft/TypeScript#30134中。

因此可以推斷泛型類型參數:

declare function foo<T>(x: (n: number) => T): T
foo((n: number) => ({ a: n })) // T inferred as {a: number}

並且可以推斷出未注釋的 function 參數:

declare function bar(f: (x: { a: number }) => void): void;
bar(x => x.a.toFixed(1)) // x inferred as {a: number}

並且有一些能力可以同時執行這兩項操作,特別是如果您要求從多個 function arguments 進行推斷並且推斷流程從左到右:

declare function baz<T>(x: (n: number) => T, f: (x: T) => void): void;
baz((n) => ({ a: n }), x => x.a.toFixed(1))
// n inferred as number, T inferred as {a: number}, x inferred as {a: number}

但有些情況下這不起作用。 在 TypeScript 4.7 之前,您只有一個 function 參數的以下變體將無法按需要進行推斷:

declare function qux<T>(arg: { x: (n: number) => T, f: (x: T) => void }): void;
qux({ x: (n) => ({ a: n }), f: x => x.a.toFixed(1) })
// TS 4.6, n inferred as number, T failed to infer, x failed to infer
// TS 4.7, n inferred as number, T inferred as {a: number}, x inferred as {a: number}

這已在 TypeScript 4.7中使用microsoft/TypeScript#48538 修復 但它離完美的算法還很遠。

例如,當與映射類型的推理相結合時,這種推理就會失效。 來自映射類型的簡單推斷如下所示:

declare function frm<T>(obj: { [K in keyof T]: (n: number) => T[K] }): void;
frm({ p: n => ({ a: n }) });
// T inferred as {p: {a: number}}

但是嘗試將其與 function 參數的同步上下文推斷相結合,但它失敗了:

declare function doa<T>(
    obj: { [K in keyof T]: { x: (n: number) => T[K], f: (x: T[K]) => void } }
): void;
doa({ p: { x: (n) => ({ a: n }), f: x => x.a.toFixed(1) } });
// TS 4.8, T failed to infer
doa<{ p: { a: number } }>(
  { p: { x: (n) => ({ a: n }), f: x => x.a.toFixed(1) } }); // okay

因此,不幸的是,如果您嘗試用您的類型表達的關系涉及許多涉及上下文類型的復雜推理路徑,那么它可能會失敗。


您的示例代碼試圖同時從映射類型和回調參數推斷進行推斷,因此它失敗了。 好吧,像這樣的呼叫簽名

function ops<T extends { [key: string]: any }, O extends Ops<T>>(spec: O): O {
    return spec;
}

永遠不會工作,因為通用約束不能作為推理站點 請參閱微軟/TypeScript#7234 編譯器無法從O extends Ops<T>的事實推斷出T 我們需要把它改成類似

function ops<T>(spec: Ops<T>): Ops<T> { // infer from mapped type
    return spec;
}

然后我們可以得到推斷...盡管他的錯誤消息有點奇怪,因為您在InferArgs類型替換為never

const okay = ops({
    one: { // okay
        lhs: {
            type: 'string',
            from: (v: string) => String(v)
        },
        rhs: {
            type: 'number',
            from: (v: number) => v.toString()
        },
        compare: (lhs: string, rhs: string) => lhs !== rhs
    }, two: { // error!
        lhs: {
            type: 'string',
            from: (v: string) => String(v)
        },
        rhs: {
            type: 'number',
            from: (v: string) => v.toString()
        },
        compare: (lhs: string, rhs: string) => lhs !== rhs
    },
    three: { // error!
        lhs: {
            type: 'string',
            from: (v: string) => String(v)
        },
        rhs: {
            type: 'number',
            from: (v: number) => v.toString()
        },
        compare: (lhs: number, rhs: string) => lhs !== rhs
    },
    four: { // error
        lhs: {
            type: 'string',
            from: (v: string) => String(v)
        },
        rhs: {
            type: 'number',
            from: (v: number) => v.toString()
        },
        compere: (lhs: string, rhs: string) => lhs !== rhs,
    }
})

我想我可能會嘗試做到這一點,以便將失敗替換為“正確的”類型而不是never ,這可能看起來像

function ops<T>(ops:
    T & { [K in keyof T]: T[K] extends {
        lhs: { type: infer KL extends keyof ArgTypes, from: (arg: any) => infer RL },
        rhs: { type: infer KR extends keyof ArgTypes, from: (arg: any) => infer RR }
    } ? Args<KL, KR, RL, RR> : Args<keyof ArgTypes, keyof ArgTypes, unknown, unknown> }
): T { return ops };

結果是,錯誤放置稍微好一點:

const okay = ops({
    one: { // okay
        lhs: {
            type: 'string',
            from: (v: string) => String(v)
        },
        rhs: {
            type: 'number',
            from: (v: number) => v.toString()
        },
        compare: (lhs: string, rhs: string) => lhs !== rhs
    }, two: {
        lhs: {
            type: 'string',
            from: (v: string) => String(v)
        },
        rhs: {
            type: 'number',
            from: (v: string) => v.toString() // error, wrong v
        },
        compare: (lhs: string, rhs: string) => lhs !== rhs
    },
    three: {
        lhs: {
            type: 'string',
            from: (v: string) => String(v)
        },
        rhs: {
            type: 'number',
            from: (v: number) => v.toString()
        },
        compare: (lhs: number, rhs: string) => lhs !== rhs // error, wrong lhs
    },
    four: { // error. missing compare
        lhs: {
            type: 'string',
            from: (v: string) => String(v)
        },
        rhs: {
            type: 'number',
            from: (v: number) => v.toString()
        },
        compere: (lhs: string, rhs: string) => lhs !== rhs,
    }
})

如果您需要同時擁有一個內聯所有屬性的單個 object 文字,這就是我能做的最好的事情。


假設這不是您要為其提供類型的現有代碼,那么您不需要 go 這條路線。 正如您所提到的,您可以使用構建器模式等讓用戶分階段構建他們的 object,其中每個階段只需要一點推理就可以成功,例如多次使用args() 例如:

class BuildOps<T extends Record<keyof T, Args<any, any, any, any>>> {
    constructor(public ops: T) { }
    add<K extends PropertyKey, KL extends keyof ArgTypes,
        KR extends keyof ArgTypes, RL, RR>(
            key: K,
            args: Args<KL, KR, RL, RR>
        ): BuildOps<T & Record<K, Args<KL, KR, RL, RR>>> {
        return new BuildOps({ ...this.ops, [key]: args } as any);
    }
    build(): { [K in keyof T]: T[K] } {
        return this.ops;
    }
    static emptyBuilder: BuildOps<{}> = new BuildOps({})
    static add = BuildOps.emptyBuilder.add.bind(BuildOps.emptyBuilder);
}

可以像這樣使用:

const myOps = BuildOps.add("one", {
    lhs: { type: 'string', from: v => String(v) },
    rhs: { type: 'number', from: v => v.toFixed(2) },
    compare: (lhs, rhs) => lhs !== rhs
}).add("two", {
    lhs: { type: 'number', from: v => v > 3 },
    rhs: { type: 'boolean', from: v => v ? 0 : 1 },
    compare(l, r) { return (l ? 0 : 1) === r }
}).build();
/* const myOps: {
    one: Args<"string", "number", string, string>;
    two: Args<"number", "boolean", boolean, 1 | 0>;
} */

這與推理算法的功能一起工作,而不是反對它。

游樂場代碼鏈接

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM