提问人:Aidin 提问时间:2/10/2020 最后编辑:Vadim KotovAidin 更新时间:2/11/2020 访问量:1023
Typescript:如何期望精确的类实例作为函数参数
Typescript: How to Expect Exact Class Instance As Function Parameter
问:
法典
请考虑以下代码、一个基类、两个子类和一个函数,该函数采用一个子类的一个实例。
abstract class AbstractNumberHolder {
constructor(private readonly value: number) { }
getValue() { return this.value; }
}
class UserId extends AbstractNumberHolder {
equals(u: UserId) { return this.getValue() === u.getValue(); }
}
class SubscriptionCost extends AbstractNumberHolder {
equals(s: SubscriptionCost) { return this.getValue() === s.getValue(); }
}
// ~~~
function printUserId(u: UserId) {
console.log(u.getValue());
}
// ~~~
const subCost = new SubscriptionCost(50);
const uid = new UserId(1000105);
printUserId(subCost); // DOES NOT ERROR, WHILE IT SHOULD!
(上面代码的 Typescript Playground 链接)
解释
我有两个完全不同的类,它们代表两个完全不同的概念。由于它们的内部实现暂时是相似的(两者都持有一个数字值),因此我让它们扩展了一个基类。UserId
SubscriptionCost
AbstractNumberHolder
问题
问题是,Typescript 只比较类的结构,而这两个类的结构是一样的!这很糟糕......
它完全否定了我最初决定选择 Typescript 的原因。如果开发人员可以不小心将“订阅成本”而不是“用户 ID”传递给函数,而打字稿无法捕获它,那么我们为什么要费心拥有这些类型呢?!
现有的次优解决方案
我环顾四周,思考了可能的解决方案。以下是可行的解决方案,但我对其中任何一个都不满意。
解决方案 1) 手动检查使用instanceof
感谢上帝,按预期工作!所以,我可以有我的第一行:instanceof
printUserId
function printUserId(u: UserId) {
assert(u instanceof UserId, "Please pass a UserId!");
console.log(u.getValue());
}
显然,它根本不是可扩展的,对于每个函数的每个参数,我都需要编写一行这样的行!
解决方案 2) 在函数上使用类型Exact
我发现有许多创造性的(但手工制作的和非标准的)方法可以强制类型完全符合您的要求。(例如,这个 GitHub 问题的评论。
使用它,我可以强制函数精确地获取 UserId。
type ExactInner<T> = <D>() => (D extends T ? D : D);
type Exact<T> = ExactInner<T> & T;
function printUserId(u: Exact<UserId>) {
console.log(u.getValue());
}
它比第一个解决方案短得多,但我仍然需要记住将其放在代码中每个函数的每个类型化参数上!Exact<...>
解决方案 3) 为类添加细微差别
先前解决方案的一个主要挫折是,我需要更改使用这些类实例的函数。一般来说,代码中的函数多于类。
因此,尝试从不同的角度解决它,我可以为类添加细微差别,使它们不可互换!
class UserId extends AbstractNumberHolder {
private I_AM_A_USER_ID = true;
}
class SubscriptionCost extends AbstractNumberHolder {
private I_AM_A_SUBSCRIPTION_COST = true;
}
同样,此解决方案需要对每个类进行手动更改,并且看起来是多余的。(对于直接来自类工厂的类,它也不能很好地扩展。
解决方案 4) 在当时添加私人细微差别(调味/品牌)extend
我在 https://michalzalecki.com/nominal-typing-in-typescript/ 发现了这篇精彩的文章,谈论了我们的名义类型问题。此外,https://spin.atomicobject.com/2018/01/15/typescript-flexible-nominal-typing/ 对两种微妙的品牌推广方法进行了很好的比较。
根据那里的解决方案,我可以考虑将一个字符串(作为假类型)传递给 Parent 类并从中制作出细微差别/品牌/风格。
abstract class AbstractNumberHolder<T extends string> {
private _?: T;
constructor(private readonly value: number) { }
getValue() { return this.value; }
}
class UserId extends AbstractNumberHolder<"UserId"> {
equals(u: UserId) { return this.getValue() === u.getValue(); }
}
class SubscriptionCost extends AbstractNumberHolder<"SubscriptionCost"> {
equals(s: SubscriptionCost) { return this.getValue() === s.getValue(); }
}
我喜欢这个解决方案的地方在于,当我添加更多类/函数时,我需要做的更改是最小的。此外,您不会错过所需的更改(因为不是可选的)。T extends string
但是,这里仍然有一些手动工作。我希望它能自动化......
那么,对于这个问题,任何人都有更好的解决方案来保证没有开发人员会意外错过(在将来创建更多类/函数时)并且不需要每个类/函数的手动工作?
答: 暂无答案
评论