我仍在学习如何使用强类型语言(如类型记录)来完成TDD。我遇到的问题之一是,虽然使用接口可以方便地快速创建测试假货,但是如果这些接口随着功能的增加而增加,那么这些测试就会变得脆弱,因为假货需要使用与大多数测试无关的其他方法/属性进行更新。因此,我认为解决方案是为特定行为提供多个小接口,并让我的具体对象实现多个接口。
但是,现在的问题是,我希望保留一个泛型对象的集合,但是在这个集合中找到一个实现了一个也传递某些条件的特定接口的对象。
在我的例子中,我有实现Entity接口的实体。可以与一些实体进行交互,这些实体还实现了Interactable接口。
我为Interactable定义了一个类型谓词,定义如下:
function isInteractable(entity: any): entity is Interactable {
return "getInteractHint" in entity;
}假设我有一系列实体,我需要该实体来满足以下条件:
const entities: Entity[] = [ ... ]
const condition = (entity: Entity): boolean => ...some test...我想做这样的事情:
getInteractable(entities: Entity[]): Interactable | undefined {
return entities.filter(isInteractable).find(condition);
}但问题是,这不足以将结果转换为Interactable或Entity & Interactable;类型记录会抱怨Entity类型的对象没有实现Interactable。
我只剩下这个可怕的结构来确保我在最后得到正确的类型:
type InteractableEntity = Entity & Interactable;
type InteractablesOrNull = InteractableEntity | null;
const castEntities: InteractablesOrNull[] = ctx.entities.map(e => {
if (isInteractable(e)) {
return e;
}
else {
return null
}
});
const filteredInteractables: InteractableEntity[] = castEntities.filter(e => e !== null);
return filteredInteractables.find(condition);这里的中间对象使代码更难读。
有什么更好、更深思熟虑的方法来做到这一点呢?
发布于 2021-04-01 02:52:02
这个问题在微软/打字稿#29501中讨论过,它被认为是TypeScript的一个设计限制。
Array.prototype.filter()有两个呼叫签名:
interface Array<T> {
// narrowing
filter<S extends T>(
predicate: (value: T, index: number, array: T[]) => value is S,
thisArg?: any
): S[];
// non-narrowing
filter(
predicate: (value: T, index: number, array: T[]) => unknown,
thisArg?: any
): T[];
}第一个调用签名是您想要的收缩行为,您可以将用户定义类型保护功能作为回调传递给它,并且数组的返回类型会发生变化。第二个调用签名是通常的情况,在这种情况下,回调不会对类型做任何事情,并且数组的返回类型不变。
第一个调用签名是通用 in S,这是S函数输出的is子句中的类型。在您的例子中,这是Interactable。注意,S是受约束 to T (数组的元素类型)。在您的例子中,这是Entity。问题是,Entity extends Interactable不是真,所以编译器不能选择这个调用签名。因此,它返回到第二个调用签名,并且不会发生缩窄:
const oops = entities.filter(isInteractable) // Entity[]如果这两者之间存在一个调用签名,它接受一个不需要扩展S的任意类型的T,并返回一个数组,其元素是交叉口 of S和T,如下所示:
filter<S>(
predicate: (value: any, index: number, array: T[]) => value is S,
thisArg?: any
): (S & T)[];那么您的代码就不会有问题了。
const interactables = entities.filter(isInteractable)
// const interactables: (Interactable & Entity)[]因此,这里的一个解决方法是您可以尝试将您自己的调用签名声明汇入到Array中。但实际上,这个用例可能并不常见,而且您可能不想改变整个代码库只针对这种情况调用Array.filter()的方式。
这里的另一种方法是只断言 isInteractable是所需类型的值,这样编译器将选择正确的filter()调用签名。而不是(x: any) => x is Interactable,您想要的是类似于(x: Entity) => x is Interactable & Entity)的东西
const interactables = entities.filter(
isInteractable as (x: Entity) => x is Interactable & Entity)
// const interactables: (Interactable & Entity)[]如果您只在代码中做一次这样的事情,这将是我的建议。
另一种可能是将isInteractable的定义更改为更通用的类型保护,如果输入类型与输出类型没有明显的重叠,该类型的作用方式将产生一个交集:
function isInteractable<T>(entity: T): entity is
Interactable extends T ? Interactable : (Interactable & T) {
return "getInteractHint" in entity;
}新的isInteractable()将T类型的entity缩小到Interactable或Interactable & T,这取决于Interactable是否比T窄。这也解决了这个问题:
const interactables = entities.filter(isInteractable);
// const interactables: (Interactable & Entity)[]只有当您计划在多个地方使用isInteractable作为filter()谓词时,我才会建议您这样做。
发布于 2021-04-01 12:30:35
为了完整起见,我想出了一个比问题中可怕的混乱更好的解决方案(尽管我认为@jcalz的解决方案更好)。
而不是:
return entities.filter(isInteractable).find(condition);它返回一种类型的Entity,我可以这样做:
const target = entities.filter(isInteractable).find(condition);
if (target && isInteractable(target)) {
return target;
}两次调用isInteractable感觉像是违反了DRY,尽管它应该是完全安全的。
https://stackoverflow.com/questions/66897137
复制相似问题