这是打字稿错误吗?向对象添加额外 属性 时出错 "not assignable to type"
Is this a Typescript bug? Error "not assignable to type" when adding an extra property to an object
我正在使用两个 API,它们对经度坐标使用不同的 属性 名称:lon
和 lng
。我正在编写一个转换器函数,它添加了替代 属性 名称,以便可以互换使用来自两个 API 的位置。
这个函数给出了一个错误,老实说,对我来说这似乎是一个错误。我知道很多解决方法,但我有兴趣了解 为什么 会出现此错误。这是一个错误,还是我在这里遗漏了什么?
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 正确理解返回值包含 lat
和 lon
,它们都是 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;
}
Complete Typescript Playground。我发现了两种不同的方法,它们不会给出任何错误(并且也不依赖于 as
)。一种是我将它分成两个独立的函数,另一种是输入非常复杂的函数。
我想当有一个泛型类型参数 T extends A | B
并且你会得到一个形式为“T & X
is not assignable to T & Y
”的错误,其中 X
和Y
等价但不完全相同,可能是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
不仅可以通过向LatLng
或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
,都是numeric literal类型。函数 fillLonLng1
旨在 return 类型 typeof crazyTown & LatLon & LatLng
的值。这要求输出 lng
值是传入的 1
,但不幸的是你在运行时得到了 0
。因此,编译器对之后的代码(诚然 非常不可能 )感到满意,这些代码看起来像是您在编译时操纵 string
但实际上会引发运行时错误。哎呀
一般来说,编译器甚至不会尝试验证具体值对未指定泛型类型的可分配性,因此很有可能编写一个 100% 安全但仍会被编译器拒绝的实现。在您的情况下,通过 Omit
实现的 fillLonLng2()
函数具有更安全的类型签名,但是如果您以与 fillLonLng1()
相同的方式实现它,编译器将无法判断。
而且,坦率地说,即使是上面的“正确”错误,您也可能不想担心,因为某人 运行 陷入这种边缘情况的可能性太低了。无论哪种方式,解决方案都是一样的:使用 type assertion 并继续:
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 };
}
我正在使用两个 API,它们对经度坐标使用不同的 属性 名称:lon
和 lng
。我正在编写一个转换器函数,它添加了替代 属性 名称,以便可以互换使用来自两个 API 的位置。
这个函数给出了一个错误,老实说,对我来说这似乎是一个错误。我知道很多解决方法,但我有兴趣了解 为什么 会出现此错误。这是一个错误,还是我在这里遗漏了什么?
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 正确理解返回值包含 lat
和 lon
,它们都是 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;
}
Complete Typescript Playground。我发现了两种不同的方法,它们不会给出任何错误(并且也不依赖于 as
)。一种是我将它分成两个独立的函数,另一种是输入非常复杂的函数。
我想当有一个泛型类型参数 T extends A | B
并且你会得到一个形式为“T & X
is not assignable to T & Y
”的错误,其中 X
和Y
等价但不完全相同,可能是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
不仅可以通过向LatLng
或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
,都是numeric literal类型。函数 fillLonLng1
旨在 return 类型 typeof crazyTown & LatLon & LatLng
的值。这要求输出 lng
值是传入的 1
,但不幸的是你在运行时得到了 0
。因此,编译器对之后的代码(诚然 非常不可能 )感到满意,这些代码看起来像是您在编译时操纵 string
但实际上会引发运行时错误。哎呀
一般来说,编译器甚至不会尝试验证具体值对未指定泛型类型的可分配性,因此很有可能编写一个 100% 安全但仍会被编译器拒绝的实现。在您的情况下,通过 Omit
实现的 fillLonLng2()
函数具有更安全的类型签名,但是如果您以与 fillLonLng1()
相同的方式实现它,编译器将无法判断。
而且,坦率地说,即使是上面的“正确”错误,您也可能不想担心,因为某人 运行 陷入这种边缘情况的可能性太低了。无论哪种方式,解决方案都是一样的:使用 type assertion 并继续:
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 };
}