网站搜索

typescript - TS2322:泛型未正确传播


我们使用 JSON:API 作为我们的 API 的主要序列化模式。简而言之,它要求服务器的 JSON 响应必须具有一个顶级的 data 属性,该属性可以包含单个实体 或一个实体数组:

// Single item
{ "data": { "id": 1 } }

// Collection of items
{ "data": [
    { "id": 1 },
    { "id": 2 }
] }

我将这个模式编码成 TypeScript 类型,这样我们就可以正确地为 API 响应类型化。这产生了以下结果:

// Attributes as the base type for our entities
type Attributes = Record<string, any>;

// Single resource object containing attributes
interface ResourceObject<D extends Attributes> {
    attributes: D;
}

// Collection of resource objects
type Collection<D extends Attributes> = ResourceObject<D>[];

// Resource object OR collection, depending on the type of D
type ResourceObjectOrCollection<D extends Attributes | Attributes[]> = D extends Array<infer E>
    ? Collection<E>
    : ResourceObject<D>;

// A response with a resource object or a collection of items of the same type
interface ApiResponse<T> {
    data: ResourceObjectOrCollection<T>;
}

一个响应总是包含一个 data 属性,该属性可以是单个资源对象,也可以是一组资源对象。
一个资源对象总是包含一个 attributes 属性,该属性持有任意实体属性。这里所有类型定义的目的是通过将实体接口作为泛型 T 传递来传播属性结构,如以下示例所示:

interface Cat {
    name: string;
}

function performSomeApiCall<Ret>(uri: string): Ret {
    return JSON.parse(''); // Stub, obviously
}

function single<T extends Attributes = Attributes>(uri: string): ApiResponse<T> {
    return performSomeApiCall<ApiResponse<T>>(uri);
}

single 函数从返回单个实体的端点获取响应——比如 /api/cats/42。因此,通过传递 Cat 接口,数据类型会正确解析为单个资源对象:

const name: string = single<Cat>('/api/cats/42').data.attributes.name;

我们也可以定义一个返回多个实体的函数:

function multiple<T extends Attributes = Attributes>(uri: string): ApiResponse<T[]> {
    return performSomeApiCall<ApiResponse<T[]>>(uri);
}

此函数返回一个 T 的集合,因此以下内容也是有效的:

const names: string[] = multiple<Cat>('/api/cats').data.map(item => item.attributes.name);

然而,不起作用的是直接定义一个获取单个实体属性的函数,而这正是我不理解的地方:

function singleAttributes<T extends Attributes = Attributes>(uri: string): T {
    return single<T>(uri).data.attributes;
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
}

类型 'Record' 不能赋值给类型 'T'.
'Record' 可以赋值给类型 'T' 的约束,但 'T' 可能会被实例化为约束 'Record' 的不同子类型。(2322)

为什么 Typescript 能理解 single 函数返回的是 T 的资源对象,但对 singleAttributes 却不能?在某些地方,它似乎放弃了泛型类型,而使用了 Attributes 的基础类型,但我不明白原因或发生的地方。

查看游乐场链接以获取问题演示。

答案#1

返回类型

function singleAttributes<T extends Attributes = Attributes>(uri: string): T {
    return single<T>(uri).data.attributes; < --- Record<string, any>
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
}

实际上是 Record<string, any>

看下一个例子:

function singleAttributes<T extends Attributes>(uri: string): T {
    let ret = single<T>(uri).data.attributes

    let t: T = null as any;
    ret = t; // ok
    t = single<T>(uri).data.attributes // error
}

类型 'Record' 不能分配给类型 'T'。'Record' 可分配给类型 'T' 的约束,但 'T' 可能会被实例化为约束 'Record' 的不同子类型。

T 可以赋值给返回值,但返回值不能赋值给 T。这就是为什么你可以将 T 用作返回类型的显式类型

T 可能是更广泛的类型。

看例子:

type WiderType = Record<string | symbol, any> extends Record<string, any> ? true : false // true


type WiderType2 = Record<string, any> extends Record<string | symbol, any> ? true : false // true

当你期望 Record<stirng, any> 时,T 也可以是 Record<string | symbol, any>。甚至可以是 Record<string | symbol | number, any>

为了让它起作用

从一开始就使用更宽的字体:

function singleAttributes<T extends Record<string | symbol | number, any>>(uri: string): T {
    return single<T>(uri).data.attributes
}

或移除显式返回类型

我会说 Record<string, any> 和 Record<number, any> 几乎是一样的,因为根据 JavaScript 规范,它会被推断为字符串。

这里你有很棒的解释

这是按照预期工作的,是 #16368 中引入的更严格检查的结果。在你的示例中,IMyFactoryType(或接口,它们在结构上是相同的)表示一个函数,该函数应该返回从 IMyObject 派生的任何类型的精确定义的值,尽管该函数实际上没有涉及 T 的任何参数,因此无法确定 T 是什么并创建相应的返回值。换句话说: