【问题标题】:formatPerson(JsonBody<T extend Person>): T, is assignable to the constraint of type 'T', but could be instantiated with a different subtypeformatPerson(JsonBody<T extend Person>): T,可分配给类型“T”的约束,但可以用不同的子类型实例化
【发布时间】:2021-06-19 11:18:00
【问题描述】:

我正在尝试以递归方式将日期字符串格式化为日期对象,但出现错误:

type JsonBody<T> = T extends Date
  ? string
  : T extends (infer U)[]
  ? JsonBody<U>[]
  : T extends object
  ? { [P in keyof T]: JsonBody<T[P]> }
  : T;

type Person = {
  name: string;
  friend?: Person;
  createdAt: Date;
};

type PersonWithFriend = Omit<Person, "friend"> & Required<Pick<Person, "friend">>;

function formatPerson<T extends Person>(body: JsonBody<T>): T {
  return {
    ...body,
    friend: body.friend && formatPerson(body.friend),
    createdAt: new Date(body.createdAt)
  };
  // Type 'JsonBody<T> & { friend: Person | undefined; createdAt: Date; }' is not assignable to type 'T'.
  //  'JsonBody<T> & { friend: Person | undefined; createdAt: Date; }' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Person'.
}

const one: JsonBody<Person> = { name: 'One', createdAt: '2021-01-21 00:59:11.07+00' };
const two: JsonBody<PersonWithFriend> = { name: 'One', friend: { name: 'Two', createdAt: '2021-01-21 00:59:11.07+00' }, createdAt: '2021-01-21 00:59:11.07+00' };
const oneFormatted = formatPerson(one).createdAt; // should be Date
const twoFormatted = formatPerson(two).friend.createdAt; // should be Date

Playground

为什么formatPerson(JsonBody&lt;T extend Person&gt;) 不返回T 以及为什么T 不变成PersonPersonWithFriend,这取决于传递的参数?

提前感谢您的帮助。

【问题讨论】:

  • 您应该将..body 编辑为...body
  • 糟糕,感谢您的提醒。我修复了它并更新了错误和游乐场。

标签: typescript


【解决方案1】:

问题在于泛型函数的类型参数由函数的调用者指定,并且有多种方法可以扩展像Person 这样的类型。您可以添加新属性,但也可以缩小现有属性的类型。如果编译器无法验证该实现是否符合调用者可能指定的任何内容,则该实现内部将出现编译器错误。

例如:

interface SpecialDate extends Date {
  specialOccasion: string;
}
interface PersonWithSpecialDate extends Person {
  createdAt: SpecialDate;
}

const expectedPersonWithSpecialDate =
  formatPerson<PersonWithSpecialDate>({ name: "", createdAt: "" });

expectedPersonWithSpecialDate.createdAt.specialOccasion.toUpperCase(); // compiles, but 
// runtime error

这里调用者出于某些只有他们自己知道的奇怪原因决定向formatPerson() 询问PersonWithSpecialDate,其createdAt 属性本身将具有specialOccasion 类型为string 的属性。 formatPerson() 的调用签名声称能够做这样的事情,因为 PersonWithSpeicalDate extends Person。所以调用者返回了一个声称是PersonWithSpecialDate的东西,一切看起来都很好,直到它在运行时爆炸。

因此实现中的警告是正确的。编译器告诉您,它不能确定您返回的内容是否可以用作T。请参阅Why can't I return a generic 'T' to satisfy a Partial<T>?,了解有关此情况的更明确的 Stack Overflow Q/A(因为它来自 TS 团队负责人)。


那你能做什么?

这里最简单的做法就是告诉编译器您不关心这种可能性。是的,从技术上讲,T 可能是一些您无法返回的奇怪事情,但您不太可能担心。在这种情况下,您可以使用type assertion 表示“相信我,这是T 类型的值”:

function formatPerson<T extends Person>(body: JsonBody<T>): T {
  return ({
    ...body,
    friend: body.friend && formatPerson(body.friend),
    createdAt: new Date(body.createdAt)
  }) as T; // no error now
}

肯定有一种方法可以重写它,以使调用签名完全准确(人们将无法要求它提供它无法提供的东西),但编译器可能仍然无法验证实现努力满足它。编译器不太擅长理解依赖于未指定泛型的条件类型的可分配性(有关此问题的可能规范问题,请参见microsoft/TypeScript#33912)。因此,与其努力尝试这样做,我将在这里停下来。如果我发现一些足够容易写出编译器可以实际验证的东西,我可能会稍后回来编辑。

Playground link to code

【讨论】:

    猜你喜欢
    • 2019-12-13
    • 2020-11-25
    • 2021-12-10
    • 1970-01-01
    • 2023-01-11
    • 2022-01-20
    • 2021-09-21
    • 1970-01-01
    • 2021-02-13
    相关资源
    最近更新 更多