我準備了一個 Angular 服務的簡化示例,它帶有多個允許通過方法覆寫的通用型別。我在這里面臨的問題是=vsextends并在引數中輸入。出于某種原因,它=非常適合回傳型別,但引數型別需要extends.
class Service<
RequestBody,
ResponseBody,
CreateRequestBody = RequestBody,
CreateResponseBody = ResponseBody,
>
{
public createOne<
$CreateRequestBody extends CreateRequestBody,
$CreateResponseBody = CreateResponseBody
>(body : $CreateRequestBody) : $CreateResponseBody
{
return null;
}
public createTwo<
$CreateRequestBody = CreateRequestBody,
$CreateResponseBody = CreateResponseBody
>(body : $CreateRequestBody) : $CreateResponseBody
{
return null;
}
}
以下是反映問題的示例,我想在createTwo()弄清楚問題后使用該方法。此時它不會驗證 body 的型別。
const service: Service<{ foo: string }, void> = new Service<{ foo: string }, void>();
// working
service.createOne({
foo: 'test'
});
// not fine as the types are not compatible
service.createOne<void, void>({
bar: 'test'
});
// not fine as everything can be set for body - typing not working for some reason
service.createTwo({
bar: 'test'
});
// fine as the local override works
service.createTwo<{ bar: 'test' }, void>({
bar: 'test'
});
這是一個TypeScript Playground來看看編譯器做了什么。
uj5u.com熱心網友回復:
首先,讓我們洗掉 on 的后兩個型別引數Service,因為它們似乎除了作為現有請求/回應主體型別的“副本”之外沒有其他用途:
class Service<Rq, Rs> { /* ... */ }
請注意,我使用Rq和Rs分別表示原始請求和回應正文型別,因為我傾向于遵循(不可否認有時必須閱讀)約定,其中泛型型別引數具有一兩個短字符名稱,因此它們可以區別于特定型別。
因此,對于create()方法,您通常想要的是body型別Rq為 ,回傳型別為Rs。但是,您還希望能夠通過在呼叫時顯式指定型別來覆寫這些型別,例如create<NewRequestType, NewResponseType>(body),它具有body型別NewRequestType并回傳NewResponseType.
免責宣告:在這一點上,我想指出,很難想象這個函式的任何實作可能是型別安全的,因為運行時的實作對編譯器傳入的型別一無所知。例如,create<string, number>("hey")應該回傳 anumber和create<string, boolean>("hey")應該回傳 a boolean,但是這兩個呼叫都編譯為 JavaScript create("hey"),因此即使在原則上也沒有辦法在所有情況下都準確。似乎您只想讓呼叫者使用這些型別引數,就像他們使用型別斷言一樣,例如create("hey") as number或create("hey") as boolean,這至少是一種明確且眾所周知的方式來承認您正在放棄型別安全。但是為了取得進展,我將考慮超出范圍的實作的型別安全,我不會進一步詳述這一點。
因此,您需要通用呼叫簽名,例如<RqO, RsO>(body: RqO) => RsOwhereRqO和RsO是所需的覆寫型別,但如果您不指定它們,它們分別回退到Rq和Rs。但是您將遇到的最大障礙是,如果您不指定型別引數,編譯器將嘗試推斷它們。無論您傳入 for ,body都會導致編譯器推斷 for 的型別RqO,因為它body是for的推斷站點RqO。此外,如果您碰巧create()在回傳型別具有預期背景關系型別的地方呼叫,編譯器將推斷該型別 for RsO,因為背景關系回傳型別是RsO. 這不是你想要的;要禁用型別推斷RqO和RsO。如果您可以說不body應該使用它來推斷RqO并且不應該使用背景關系回傳型別來推斷,那就太好了RsO。
沒有“關閉”推理站點的官方方法,但是在microsoft/TypeScript#14829有一個功能請求,它要求某種方法來實作型別函式,例如NoInfer<T>它T最終計算為 ,但不能用于推斷T。幸運的是,有一些建議至少適用于某些用例。我通常使用的一個來自這個評論:
type NoInfer<T> = [T][T extends any ? 0 : never];
以防萬一:這里我們使用依賴于未指定泛型的分布式條件型別是deferred的事實,因此T extends any ? 0 : never在T修復之后才進行評估。無論T變成什么,T extends any ? 0 : never最終都會評估為0,并且[T][0]只是T(因為您正在索引到單元素元組型別的第一個元素)。
這意味著我們可以這樣寫create():
public create<RqO = Rq, RsO = Rs>(
body: NoInfer<RqO>
): NoInfer<RsO> {
return null!;
}
我們使用泛型引數的默認值的Rq和Rs為RqO和RsO,不指定型別引數,當,當推斷失敗(它肯定會),他們將回落到默認值。
那么,讓我們試一試:
const service = new Service<{ foo: string }, string>();
首先,我們不會指定請求和回應型別,希望編譯器會回退到Rq和Rs型別service:
const ret = service.create({
foo: 'test'
}).toUpperCase(); // okay
service.create({
bar: 'test' }); // error, {bar:string} is not {foo: string}
const ret2: number = service.create({
foo: 'test'
}); // error, number is not string
看起來挺好的。現在我們將指定型別,希望編譯器會使用它們而不是來自service:
const ret3 = service.create<{ bar: string }, number>(
{ bar: "test" }).toFixed(2); // okay
const ret4 = service.create<{ bar: string }, number>(
{ foo: "test" }
); // error, {foo:string} is not {bar: string}
const ret5: string = service.create<{ bar: string }, number>(
{ bar: "test" }
看起來也不錯。所以這是一個可行的解決方案。
請注意,還有其他方法可以獲取非推理型別引數的用法。您可以改為添加其他型別引數:
public create<
RqO = Rq,
RsO = Rs,
RqI extends RqO = RqO,
RsI extends RsO = RsO
>(body: RqI): RsI {
return null!;
}
這是有效的,因為編譯器將使用body來推斷RqI(想想“隱式”覆寫)和回傳型別來推斷RsI,但約束是RqI extends RqO和RsI extends RsO,反之亦然。所以對RqI和的推論RsI不會影響RqO和RsO。您可以驗證其行為是否相似。我不會詳細介紹,但這是一種有時NoInfer<T>行不通的方法。
Playground 鏈接到代碼
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/363416.html
上一篇:根據列舉值回傳泛型方法中的特定型別,沒有反射或運行時動態
下一篇:C#-將泛型委托轉換為非泛型委托
