【问题标题】:Typescript generic with constraint doesn't have key带有约束的打字稿通用没有键
【发布时间】:2020-03-27 07:37:52
【问题描述】:

我正在尝试创建一组对通用T 进行操作的函数,这些函数必须实现和接口IDocument。虽然这通常似乎有效,但 TypeScript 似乎无法识别 T 必须具有 IDocument 的键。

这是一个最小的例子:

interface IDocument
{
    _id: number;
}

function testGeneric<T>(v: { [P in keyof T]?: T[P] })
{ }

function testConstraint<T extends IDocument>(doc: T)
{
    // this works
    console.log(doc._id);

    // this works
    testGeneric<IDocument>({ _id: doc._id });

    // this fails
    // Argument of type '{ _id: number; }' is not assignable to parameter of type '{ [P in keyof T]?: T[P] | undefined; }'.
    testGeneric<T>({ _id: doc._id });
}

您可以在 TypeScript 游乐场here 上现场观看。

我很困惑为什么这不起作用,因为在我的testConstraint 函数中似乎T 总是有一个_id 键,因为它必须实现IDocument。事实上,如果我使用T 参数并访问_id 属性,它就可以正常工作。

请注意,testGeneric 函数位于我不拥有的库中,因此我无法更改该签名。

我在这里做错了什么?我是否需要使用不同的约束来表达T 必须拥有IDocument 拥有的每个键?

【问题讨论】:

标签: typescript


【解决方案1】:

原始示例有一个像{_id: 10} 这样的值分配给像Partial&lt;T&gt; 这样的泛型类型,其中T extends IDocument。在这种情况下,您可以证明编译器的抱怨是正确的,因为有些类型 T 扩展了 IDocument,其中 10 不是有效属性。这与this question 中的问题基本相同。


{_id: doc._id} 分配给泛型类型Partial&lt;T&gt; 的新示例,其中T extends IDocument 仍然会产生错误,即使它绝对应该是安全的。编译器是unable to verifyPick&lt;T, "_id"&gt; 可分配给Partial&lt;T&gt;。这是(与)an open issue 相关的;编译器目前无法进行必要的类型分析来确保这一点。在您确定某些东西是安全的(并且您已经检查了两次和三次)但编译器不安全的情况下,您可能需要使用type assertion

testGeneric<T>({ _id: doc._id } as Partial<T>); // okay now

因此,在 testConstraint() 的实现中,您可能需要使用类型断言或等价物(例如具有更宽松实现签名的单个调用签名 overload)。


最后,你说你想阻止某人使用T 调用testConstraint&lt;T&gt;(),其属性比IDocument 上的属性。这比T extends IDocument 更具限制性,并且在 TypeScript 中表示更麻烦,其中属性缩小是子类型的自然组成部分。您可以通过使通用约束包含 conditional 映射类型来做到这一点,如下所示:

function testConstraint<
    T extends IDocument &
    { [K in keyof IDocument]: IDocument[K] extends T[K] ? T[K] : never }>(doc: T): void;
function testConstraint(doc: IDocument) {
    testGeneric({ _id: doc._id });
}

这里我们将T 限制为IDocumentIDocument 的每个属性与T 的相应属性进行比较的类型。如果T 上的那个不比IDocument 上的那个窄,那就太好了。否则,约束中的属性将一直缩小到neverT 可能不会匹配。

调用签名非常复杂,以至于实现中的类型真的会让编译器感到困惑。这就是为什么我使它成为一个重载,并将实现签名放宽为完全非泛型。您可能可以为实现做一些通用的事情,但关键是您可能应该根据类型分别对待调用端和实现。

让我们看看这个函数的实际作用:

interface TheVeryFirstDocument extends IDocument {
    _id: 1
}
declare const tv1d: TheVeryFirstDocument;
testConstraint(tv1d); // error!
// --------->  ~~~~~
//  Types of property '_id' are incompatible.
//    Type '1' is not assignable to type 'never'.(

如我们所愿,这给出了一个错误,而以下工作没有错误:

declare const doc: IDocument;
testConstraint(doc); // okay

interface ExtendedDocument extends IDocument {
    title: string;
    numPages: number;
}
declare const xDoc: ExtendedDocument;
testConstraint(xDoc); // okay

好的,希望对您有所帮助;祝你好运! Link to code

【讨论】:

    猜你喜欢
    • 2020-07-29
    • 2021-11-08
    • 1970-01-01
    • 2018-09-17
    • 2022-01-10
    • 2018-03-12
    • 1970-01-01
    • 2018-03-15
    • 2017-06-16
    相关资源
    最近更新 更多