这是一个打字稿错误吗?向对象添加额外属性时出错"not assignable to type"



我正在使用两个API,它们对经度坐标使用不同的属性名称:lonlng。我正在编写一个转换器函数,它添加了可选的属性名称,以便两个API中的位置可以互换使用。

这个函数给出了一个错误,老实说,这个错误对我来说似乎是个错误。我知道很多解决方法,但我有兴趣了解为什么会出现此错误。这是一个bug,还是我遗漏了什么?

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;
}

完成打字游戏。我发现了两种不同的方法,它们不会产生任何错误(也不依赖于as)。一个是我把它分解成两个单独的函数,另一个是愚蠢而复杂的打字。

我认为当有一个泛型类型参数T extends A | B时,您会得到一个形式为"的错误;CCD_ 10不可分配给CCD_;其中XY是等效的,但不完全相同,这可能是microsoft/TypeScript#24688中提到的编译器错误。

您可以通过将LatLon & LatLng重构为编译器认为与{ 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();
}

以下注意事项仍然有效(即使它们不像我最初想象的那样直接适用)


如本问题中所述,声称要创建泛型类型值的函数的问题在于,函数的调用方可以将泛型指定为满足约束的任何类型,并且调用方可能会以实现者没有预料到的方式这样做。

例如,T extends LatLng | LatLon不仅可以通过LatLngLatLon添加新的属性来满足,还可以通过缩小现有的

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具有lonlng,两者都是数字文字类型。函数fillLonLng1旨在返回一个类型为typeof crazyTown & LatLon & LatLng的值。这要求输出lng值为传入的1,但不幸的是,您在运行时会得到一个0。因此,编译器对之后的代码(诚然非常不可能)感到满意,这看起来像是在编译时操纵string,但实际上会引发运行时错误。哎呀。


一般来说,编译器甚至不会尝试验证具体值对未指定泛型类型的可分配性,因此很可能编写一个100%安全但仍会被编译器拒绝的实现。在您的情况下,通过Omit实现的fillLonLng2()函数有一个更安全的类型签名,但如果您以与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;
}
}

游乐场链接到代码

这应该做:

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 };
}

最新更新