我正在尝试创建一组在通用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
拥有的每个键?
答案 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