簡體   English   中英

這是 Typescript 錯誤嗎? 向 object 添加額外屬性時出現“不可分配給類型”錯誤

[英]Is this a Typescript bug? Error “not assignable to type” when adding an extra property to an object

我正在使用兩個 API,它們對經度坐標使用不同的屬性名稱: lonlng 我正在編寫一個轉換器 function,它添加了替代屬性名稱,以便可以互換使用兩個 API 中的位置。

這個 function 給出了一個錯誤,老實說,這對我來說似乎是一個錯誤 我知道很多解決方法,但我有興趣了解為什么會發生此錯誤。 這是一個錯誤,還是我在這里遺漏了什么?

const fillLonLng1 = <T extends LatLng | LatLon>(loc: T): T & LatLon & LatLng => {
    if ( isLatLon(loc) ) {
        return {...loc, lng: loc.lon};
    } else {
        return {...loc, lon: loc.lng};
    }
}

第一個return語句給出了一個錯誤:

Type 'T & { lng: number; lat: number; lon: number; }' is not assignable to type 'T & LatLon & LatLng'.
Type 'T & { lng: number; lat: number; lon: number; }' is not assignable to type 'LatLon'.(2322)

Typescript 正確理解返回值包含一個latlon都是number 我不明白這怎么可能不能分配給LatLon ,它被定義為:

interface LatLon {
    lat: number;
    lon: number;
}
const isLatLon = <T extends Partial<LatLon>>(loc: T): loc is T & LatLon => {
    return loc.lat !== undefined && loc.lon !== undefined;
}

完成 Typescript 游樂場 我發現了兩種不同的方法,它們不會給出任何錯誤(也不依賴as任何一個)。 一種是我將其分解為兩個獨立的函數,另一種是具有愚蠢復雜的類型。

我認為當有一個泛型類型參數T extends A | B T extends A | B並且您收到“ T & X is not assignable to T & Y ”形式的錯誤,其中XY等效但不相同,這可能是microsoft/TypeScript#24688中提到的編譯器錯誤。

您可以通過將LatLon & LatLng重構為編譯器認為與{ lng: number; lat: number; lon: number; }相同的類型來說服編譯器接受它。 { lng: number; lat: number; lon: number; } { lng: number; lat: number; lon: number; }

interface LatLonLng extends LatLon, LatLng { }
const fillLonLng1 = <T extends LatLng | LatLon>(loc: T): T & LatLonLng => {
    if (isLatLon(loc)) {
        return { ...loc, lng: loc.lon };
    } else if (isLatLng(loc)) {
        return { ...loc, lon: (loc as LatLng).lng };
    } else throw new Error();
}

下面的警告仍然有效(即使它們不像我最初想象的那樣直接適用)


正如在這個問題中提到的,function 聲稱要制作泛型類型的值的問題是,function 的調用者可以將泛型指定為他們想要滿足約束的任何內容,並且調用者可以在實現者沒有預料到的方式。

例如, T extends LatLng | LatLon 不僅可以通過向LatLngT extends LatLng | LatLon添加新屬性,還可以通過縮小現有屬性來滿足LatLon

const crazyTown = { lat: 0, lon: 0, lng: 1 } as const;
const crazierTown = fillLonLng1(crazyTown);
const crazyTuple = [123, "hello"] as const;
const crazyString = crazyTuple[crazierTown.lng].toUpperCase(); // no compile error, but:
// RUNTIME ERROR!  crazyTuple[crazierTown.lng].toUpperCase is not a function

在這里, crazyTown有一個lon和一個lng ,它們都是數字文字類型。 function fillLonLng1聲稱返回typeof crazyTown & LatLon & LatLng類型的值。 這要求傳入的 output lng值為1 ,但不幸的是,您在運行時得到了0 因此,編譯器對之后的(誠然非常不可能的)代碼感到滿意,看起來您在編譯時正在操作string ,但實際上會引發運行時錯誤。 哎呀。


一般來說,編譯器甚至不會嘗試驗證具體值可分配給未指定的泛型類型,因此很有可能編寫一個 100% 安全但仍會被編譯器拒絕的實現。 在您的情況下,通過Omit實現的fillLonLng2() function 具有更安全的類型簽名,但如果您以與fillLonLng1()相同的方式實現它,編譯器將無法判斷。

而且,坦率地說,即使是上面的“正確”錯誤也可能是您不想擔心的事情,因為有人遇到這樣的邊緣情況的可能性太低了。 無論哪種方式,解決方案都是一樣的:使用類型斷言並繼續:

const fillLonLng1 = <T extends LatLng | LatLon>(loc: T): T & LatLon & LatLng => {
    if (isLatLon(loc)) {
        return { ...loc, lng: loc.lon } as any;
    } else {
        return { ...loc, lon: (loc as LatLng).lng } as any;
    }
}

或者

const fillLonLngSafer = <T extends LatLng | LatLon>(loc: T
  ): Omit<T, keyof LatLng | keyof LatLon> & LatLon & LatLng => {
    if (isLatLon(loc)) {
        return { ...loc, lng: loc.lon } as any;
    } else {
        return { ...loc, lon: (loc as LatLng).lng } as any;
    }
}

Playground 代碼鏈接

這應該這樣做:

interface LatLon {
  lat: number;
  lon: number;
}
namespace LatLon {
  export function is(loc: object): loc is LatLon {
    return ['lat', 'lon'].every(key => key in loc && loc[key] !== undefined);
  }
}

type LatLng = { lat: number; lng: number; };

export function fillLonLng(loc: LatLng | LatLon): LatLon & LatLng {
  return LatLon.is(loc) ? { ...loc, lng: loc.lon } : { ...loc, lon: loc.lng };
}

暫無
暫無

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

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