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

Is this a Typescript bug? Error "not assignable to type" when adding an extra property to an object

我正在使用两个 API,它们对经度坐标使用不同的 属性 名称:lonlng。我正在编写一个转换器函数,它添加了替代 属性 名称,以便可以互换使用来自两个 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 正确理解返回值包含 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;
}

Complete Typescript Playground。我发现了两种不同的方法,它们不会给出任何错误(并且也不依赖于 as)。一种是我将它分成两个独立的函数,另一种是输入非常复杂的函数。

我想当有一个泛型类型参数 T extends A | B 并且你会得到一个形式为“T & X is not assignable to T & Y”的错误,其中 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有一个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;
    }
}

Playground link to code

应该这样做:

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