带有约束的通用打字稿没有密钥

时间:2019-12-02 16:40:19

标签: typescript

我正在尝试创建一组在通用T上运行的功能,这些通用IDocument必须实现并与IDocument接口。尽管这通常似乎可行,但似乎TypeScript无法识别T必须具有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 }); } 的键。

这是一个最小的示例:

testConstraint

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

我对为什么不起作用感到困惑,因为在我的T函数中,_id似乎总是有一个IDocument键,因为它必须实现{{1} }。实际上,如果我使用一个T参数并访问_id属性,它将很好地工作。

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

我在这里做错了什么?我是否需要使用其他约束条件来表示T必须具有IDocument拥有的每个键?

1 个答案:

答案 0 :(得分:1)

原始示例的值{_id: 10}分配给通用类型,例如Partial<T>,其中T extends IDocument。在这种情况下,您可以证明编译器是正确的,因为有些类型T扩展了IDocument,其中10不是有效的属性。这基本上与this question中的问题相同。


新示例中,您将{_id: doc._id}分配给通用类型Partial<T>,其中T extends IDocument仍然会产生错误,即使绝对安全。 unable to verify的编译器是Pick<T, "_id">可分配给Partial<T>的编译器。这(与an open issue相关);编译器目前无法进行必要的类型分析来确保这一点。如果您确定某些内容是安全的(并且已经对它进行了两次和三次检查),但编译器却没有,那么您可能需要使用type assertion

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

因此在testConstraint()的实现中,您可能需要使用类型声明或等效声明(例如,具有较宽松的实现签名的单个调用签名overload)。


最后,您说过,您实际上想阻止某人通过testConstraint<T>()来呼叫T,该IDocument的属性比T extends IDocument的属性更窄。这比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 }); } 更具限制性,并且在TypeScript中表示麻烦,因为TypeScript中的属性缩窄是子类型化的自然部分。您可以通过使通用约束包含conditional映射类型来做到这一点,就像这样:

T

在这里,我们将IDocument约束为IDocument和类型,其中将T的每个属性与T的对应属性进行比较。如果IDocument上的那个不比never上的那个窄。否则,约束中的属性将一直缩小到T,而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

正如我们所希望的那样,这会产生错误,而以下内容可以正常工作:

punctuator

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