typescript 伪运算符重载

5t7ly7z5  于 2022-11-26  发布在  TypeScript
关注(0)|答案(2)|浏览(301)

JS/TS对StringNumberBoolean类型进行自动装箱和拆箱,允许在同一表达式中混合使用文字和对象,而无需显式转换,例如:
const a = "3" + new String("abc");
我尝试通过提供一个定制类Long来为bigintnumber实现类似的功能:

class Long {
    public constructor(private value: bigint | number) { }

    public valueOf(): bigint {
        return BigInt(this.value);
    }
}

const long = new Long(123);
console.log(456n + long);

这样做效果很好(打印579n),但是会导致我的linter和TS编译器显示最后一个表达式的错误。我可以用下面的注解来抑制它们:

// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
console.log(456n + long);

但对于整个应用程序来说,这并不是一个好的解决方案。
有没有办法告诉Long是被当作bigint还是其他东西来处理,以避免错误?

关于为什么这样做:

我正在开发一个转换Java to Typescript的工具,希望尽可能多地支持Java语义。其只能通过使用bigint在TS中表示。主要问题是Java会像String一样自动解盒Long,我希望尽可能地支持这种语义。
对于@caTS:所以这永远不会是正常的TS代码,但总是作为java.lang.Long使用,因此不会有混淆。

0yg35tkg

0yg35tkg1#

你想要的行为,即TypeScript允许你使用一个自定义类来覆盖Object.prototype.valueOf()方法,就像它是valueOf()返回的原始类型一样,不幸的是,它不是语言的一部分。在microsoft/TypeScript#2361上有一个相当长的开放特性请求,但是它还没有被实现,而且看起来也不会很快被实现。
目前,这意味着只有一些变通方法,其中一个变通方法是对编译器撒谎,说Long有一个返回原语示例的构造签名,也就是说,你希望它具有new (value: bigint | number) => bigint;类型(或者new (value: bigint | number) => bigint & Long,这样你就可以保留添加到Long的任何额外方法或功能)。
以下是您的具体做法:

// rename
class _Long { 
    public constructor(private value: bigint | number) { }

    public valueOf(): bigint {
        return BigInt(this.value);
    }
}

// assign and assert
const Long = _Long as 
    new (value: bigint | number) => bigint & _Long;

这里我把你原来的Long构造函数重命名了,因为一旦你声明了class Long { },值Long就会自动得到一个构造函数类型,你不能改变这个类型。
然后,我将重命名的构造函数赋给所需的变量Long,并Assert它是所需的类型new (value: bigint | number) => bigint & _Long,而不是实际的类型new (value: bigint | number) => _Long
我需要使用类型Assert,因为编译器会抱怨普通赋值;它知道X1 M12 N1 X的示例类型不是X1 M13 N1 X。
好了,现在我们有了一个名为Long的类构造函数,编译器认为它产生了bigint示例。让我们测试一下:

const long = new Long(123);
// const long: bigint
console.log(456n + long); // okay, 579

看起来不错。我可以调用new Long(123),编译器认为结果是bigint。它还让我毫无怨言地使用像+这样的数学运算符,结果也是你所期望的。
所以这和我想象的差不多。不过,我通常不会故意对编译器撒谎,因为这样的谎言会在以后以奇怪的方式绊倒你。long的类型显然 * 不是 * bigint

console.log(typeof long); // "object", not "bigint"

因此,任何依赖于long实际上是bigint的操作都可能会执行编译器无法捕捉的有趣操作,因此您将看到意外的运行时行为:

const bigint1 = BigInt(4);
const bigint2 = BigInt(4);
console.log(bigint1 === bigint2); // true

const long1 = new Long(4);
const long2 = new Long(4);
console.log(long1 === long2); // false!

function copy<T extends {}>(x: T) {
    return (typeof x === "object") ? { ...x } : x
}

const bigint3 = copy(bigint1);
console.log(bigint3 + 1n) // 5

const long3 = copy(long1);
console.log(long3 + 1n) // "[object Object]1" 🤪

现在,在问题中提到的特定用例中,由于将生成所有TypeScript代码,因此您可以保证不会生成任何代码,这些代码会遇到任何障碍。但即使如此,在决定是否继续时,了解这些情况并将其考虑在内是很重要的。
Playground代码链接

ryoqjall

ryoqjall2#

除了@jcalz已经提到的问题之外,他的解决方案还有一个大问题:您不能使用Long类的任何功能。对于编译器和ESLint,由于附加的构造函数Assert,Long被视为bigint基元类型。
然而,JS有一个内置的原语解析方法,在Symbol.toPrimitive页面上有描述。在我最初的方法中,我使用了一个解决方案(valueOf),让JS自动将Long类转换为原语bigint值,这保留了Long类的所有功能。
不幸的是,使用valueOfSymbol.toPrimitive仍然需要一个显式步骤(调用数值、字符串或原语强制),因此这只是一个半完美的解决方案。

class Long {
    public constructor(private value: number) { 
    }

    public valueOf(): number {
        return this.value;
    }

    private [Symbol.toPrimitive](hint: string) {
        if (hint === "number") {
            return this.value;
        } else if (hint === "string") {
            return this.toString();
        }

        return null;
    }
}

const long = new Long(123);
console.log(456 + +long);

(playground)。注意long变量前面的额外+,它触发了数字强制。正如您所看到的,我在这里没有使用bigint,因为它以不同的方式处理。在原始代码中,不需要数字强制来使它工作(只有错误抑制)。但是,将Symbol.toPrimitivebigint一起使用是行不通的,即使使用数字强制也是如此。它只是没有实现。一个添加该功能的建议是refused years ago
所以,总的来说,对于bigint,我的问题没有解决方案(而且bigint也不是一个好的选择)。但是对于所有其他可以强制转换为基元类型(字符串或数字)的类,如果你接受显式强制,上面提到的方法可以很好地工作。

更新

我考虑了@jcalz的建议,为Long类使用一个单独的交集类型。查看这个新的playground示例以了解详细信息。虽然这看起来很有效,但它仍然显示了一些打字脚本错误,我不得不抑制这些错误。基本上所有静态成员都会引发TS错误。
此外,这个解决方案还有其他问题,比如在使用constructor.name(我必须使用它来进行反射仿真)时更改了类名(不再是LONG)。

相关问题