Code
考虑以下代码,一个基类,两个子类,以及一个采用一个子类的一个实例的函数。
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 链接 http://www.typescriptlang.org/play/index.html#code/C4TwDgpgBAogHgQwMbAJIDt0QE4B4AqAfFALxS4AihAFAJSnHUVQRzAToAmAzlPlAH4ozAFzDaAbgBQoSLEQoCxMvGRpMOJVABkfaVIQAjbsGxqoSADYJuvAILHTagHIBXALaGcACQD2lzhwoAG8pKHCLX3QTbFcUX2xqMGwASwA3BHYobAgETijLECgMy1cIMXQPL2x6YKgAXzCIgHMIYAA1BFKIOhDsttdsdChgAAsU7gA6ErKJBqlGqSsbXgBVbhxUThY2Dh4oBxiXKp9-QOwQpvCIAEdXLu5qVzF1zc5a-uBB4bGJydaOl0yr0SKCoK5-m1Ot06HNGotlrYoABlVzGJCpMDAFJRADCvhMO3YXHsjjMKDcnlOASCoQiLDuD2o3DEqPRmOxeIJwA+OS+QxG4ymAOhwPooLIwqhQJ6knmiwAZq50CgccNkil0MBXtgtk8Xhtde9LvSkFFuP4IJNLL5mk9IYCYbQ5QspEtzcAoNw0fjCWQsAB3FFo7gYlJYtW+4DUACsAAZJO7op7XCltv6IEGdXqAIxx-N5mOJqQarXZzjMn3cxNAA)
解释
我有两个完全不同的班级UserId
and SubscriptionCost
它们代表了两个完全不同的概念。由于它们的内部实现暂时相似(都持有一个数值),所以我让它们扩展一个基数AbstractNumberHolder
class.
Problem
问题是,打字稿仅比较结构 https://www.typescriptlang.org/docs/handbook/type-compatibility.html#classes类,并且这两个类具有相同的结构!而且很糟糕...
它完全否定了我最初决定选择 Typescript 的原因。如果开发人员不小心将“订阅成本”而不是“用户 ID”传递给函数,并且打字稿无法捕获它,那么我们为什么要费心拥有这些类型呢?
现有的次优解决方案
我环顾四周,思考可能的解决方案。以下是可行的解决方案,但我对其中任何一个都不满意。
解决方案1)手动检查使用instanceof
感谢上帝,instanceof
按预期工作!所以,我可以得到我的第一行printUserId
be:
function printUserId(u: UserId) {
assert(u instanceof UserId, "Please pass a UserId!");
console.log(u.getValue());
}
显然它根本不可扩展,并且对于每个函数的每个参数,我都需要编写这样的一行!
解决方案2)使用Exact
在函数上输入
我发现有许多创造性的(手工制作的和非标准的)方法可以让你的类型完全符合你的要求。 (评论这个 github 问题 https://github.com/microsoft/TypeScript/issues/12936, 例如。)
使用它,我可以强制该函数采用exactly一个用户 ID。
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://michalzalecki.com/nominal-typing-in-typescript/谈论我们的名义打字问题。还,https://spin.atomicobject.com/2018/01/15/typescript-flexible-nominal-typing/ https://spin.atomicobject.com/2018/01/15/typescript-flexible-nominal-typing/对两种微妙的品牌推广方法进行了很好的比较。
根据那里的解决方案,我可以考虑将字符串(作为假类型)传递给父类,并从中产生细微差别/品牌/风味。
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
不是可选的)。
然而,这里仍然有一些手工工作。我希望它可以自动化...
那么,对于这个问题,任何人都有更好的解决方案,保证没有开发人员会意外错过(在将来创建更多的类/函数时)并且不需要每个类/函数进行手动工作吗?