【问题标题】:I find the behavior of the the following TypeScript snippet inconsistent. Am I missing something?我发现以下 TypeScript 片段的行为不一致。我错过了什么吗?
【发布时间】:2020-11-21 08:33:11
【问题描述】:

下面,将字符串文字分配给基本(原始)类型string 的变量很好:let s3: string = "s"

但是 TypeScript 不应该禁止将字符串文字分配给非基本 String 类型的变量:let s1: String = "s"?特别是,考虑到后面的s1 instanceof Stringfalse

TS Playground link.

let s1: String = "s"; // no error here
let s2: String = new String("s"); 
let s3: string = "s";
console.log(s1 instanceof String) // false
console.log(s2 instanceof String) // true
console.log(s1 === s2); // false
console.log(s1 === s3); // true;
console.log(typeof(s1)) // "string"
console.log(typeof(s2)) // "object"
console.log(typeof(s3)) // "string"
//console.log(s3 instanceof string) // error

生成的JS代码是这样的(TS 4.0,-t ESNext t.ts):

let s1 = "s"; // no error here
let s2 = new String("s");
let s3 = "s";
console.log(s1 instanceof String); // false
console.log(s2 instanceof String); // true
console.log(s1 === s2); // false
console.log(s1 === s3); // true;
console.log(typeof (s1)); // "string"
console.log(typeof (s2)); // "object"
console.log(typeof (s3)); // "string"
//console.log(s3 instanceof String) // error

我了解这段 JavaScript 代码的工作原理,但为什么 TS 默认会这样生成它:原始值而不是 String 的隐式实例。我宁愿期待let s1 = new String("s"),或者一个错误。

我的意思是,如果变量 v 在 TypeScript 中是非基本类型 Type,我希望 v instanceof Typetrue,但 s1 不是这种情况。

这种行为是否在某处的规范中定义?

【问题讨论】:

  • 这很奇怪!如果你认为 (s1 instanceof String) 应该是真的,会导致很多问题!
  • 有趣。但是你真的在某个地方使用new String 吗? (只是好奇)
  • @AlekseyL.,不是真的,我偶然遇到了这个。此外,我在 vanilla JavaScript 中比在 TypeScript 中做的工作更多,每当我需要检查某个东西是否是字符串时,我都学会了这样做:stringVar.constructor === String。这也适用于全面和 TS (fiddle)。它不适用于跨领域,但这个可以:stringVar.constructor.name === 'String'
  • 你居然忘记了一个案例:let s4: string = String("s"); :-)
  • 我想他们故意提供了从 stringString 的隐式转换,但反之亦然。 ` 让我们:字符串 = '';让 ss = 'aaa'; s = ss; ss = s; ` 最后一行有错误Type 'String' is not assignable to type 'string'. 'string' is a primitive, but 'String' is a wrapper object. Prefer using 'string' when possible.ts(2322)

标签: javascript typescript


【解决方案1】:

首先:TypeScript 不能也不会做的一件事是采用 let s1: String = "s"; 之类的代码并将其作为 let s1 = new String("s"); 发送到 JavaScript。那是因为 TypeScript 的类型系统在 TypeScript 编译为 JavaScript 时是 erased。 TypeScript 的发射器将剥离任何特定于类型系统的功能,例如类型注释。如果剩下的是 JavaScript 目标版本中的有效 JavaScript,则将按原样发出。对于发出的 JavaScript,除了 let s1 = "s"; 之外真的没有其他选择。

它具体是一个non-goal of the TypeScript language design,用于“在程序中添加或依赖运行时类型信息,或根据类型系统的结果发出不同的代码”。因此,应放弃任何此类建议或期望。


现在:为什么他们允许您将 string 值分配给 String 类型的变量?这是microsoft/TypeScript#3448 的主题(尽管该问题的开场白认为stringString 之类的类型应该可以相互分配,而您建议它们应该相互不兼容。 .. 但出现了相同的主题,因此请参阅该问题以获取更多信息)。

目前,像string 这样的原始类型被认为可以分配给为其包装对象类型(如String)定义的接口,但反之则不然。也就是说,在 TypeScript 中,stringString 的(正确的)子类型。这一切都按预期工作,尽管至少有一位语言设计者将这种情况描述为a landmine

那么为什么上述行为(string 可分配给String)按预期工作?为了理解它,让我们来谈谈 TypeScript 的一些通常很受欢迎的特性,它们结合起来会产生这种有点不幸的行为。


首先是命名类构造函数值和它们构造的实例的接口类型之间的关系。假设我们有一个名为Foo 的类构造函数;这将在运行时存在(作为显式 ES2015 class 或作为 ES5 function),我们可以使用它来构造实例(例如,new Foo("someArg");)并针对实例进行测试(例如,val instanceof Foo)。那么,一般来说,TypeScript的静态类型系统中会有一个interface,也叫Foo,对应构造函数创建的instances的类型。因此,如果我们调用 const foo = new Foo("someArg");,那么当我们在 TypeScript IDE 中检查 foo 时,它可能会向我们显示 const foo: Foo;

当我们在 TypeScript 中编写 class 时,这种同名关系会自动发生。对于不使用class 语法的库声明,这也是通常的约定;构造函数将有一个命名接口,名为FooConstructor,带有一个新的签名,如{ new (arg: string) => Foo },然后构造函数值将被声明为declare var Foo: FooConstructor;(有关更多信息,请参阅this part of the handbook)......这相当于同样的事情:Foo 是构造函数的名称,其实例的类型为 Foo

String(以及NumberBoolean)遵循此约定。有一个 StringConstructor interfaceString 被声明为该类型的值。当我们在StringConstructor 上调用new String() 时,我们会得到一个可分配给String interface 的值。


接下来是 TypeScript 的类型系统是structural 而不是名义上的。如果类型A 和类型B 具有相同的shape(即它们的属性和方法具有相同的名称和相同的类型),那么TypeScript 认为它们是相同的type时间>。即使interface A { }interface B { } 在两个不同的地方声明并且没有相互提及,它们仍然可以是同一类型。

这与先前的功能相结合导致instanceof 的行为有些奇怪:

class Foo {
    x: string;
    constructor(x: string) {
        this.x = x;
    }
}

const foo: Foo = new Foo("x");
console.log(foo instanceof Foo); // true

const bar: Foo = { x: "x" }; // also accepted
console.log(bar instanceof Foo); // false

我可以说barFoo 类型,因为Foo 接口只关心它有一个名为xstring 类型的属性。不要求 Foo 类型的值由 Foo 构造函数构造。所以val instanceof Foo 的行为无法在 TypeScript 中清晰地表示。这类不匹配(JavaScript 关心值的出处而 TypeScript 不关心)是不幸的,但由于 TypeScript 依赖于结构兼容性,因此不容易避免。


最后,每当您尝试查看属性或调用它们的方法时,JavaScript 中的基元都会被相应类型的包装器对象包装起来。这使得像string 这样的基元外观 是具有像String 这样的接口的对象。因此,TypeScript 允许您将 string 视为 String,方法是从 String 接口给它 apparent members。这就是为什么您可以在 TypeScript 中编写 "foo".toUpperCase() 而不会发出警告。


把这三个放在一起,你的问题就会变得一团糟。 TypeScript 的String 接口只是意味着“这个东西具有与String 对象相同的属性和方法”,而不是“这个东西是通过在String 构造函数上调用new 生成的”。没有什么能阻止你写const str: String = "x";。但当然typeof str === typeof new String() 将是false,并且任何其他行为取决于原语及其包装对象之间差异的测试在 TypeScript 中都是不可观察的。这是 TypeScript 的一些不同的有用功能以令人不快的方式交互的结果。

因此recommended 从不 使用包装对象类型。如果你在代码中写了String 类型,那可能不是你想要的,所以不要这样做。这样的建议可能无法很好地替代编译器警告,但目前这似乎是可以做到的最好的。


Playground link to code

【讨论】:

    【解决方案2】:

    嗯,这实际上看起来很合乎逻辑

    字符串字面量具有与String 对象相同的所有方法,因此例如'a'.splitnew String('a').split 都存在,您可以在运行时使用它们而不会中断。在打字稿中,如果一个对象具有另一个对象所具有的所有属性,则后者可以分配给前者。

    关于比较:打字稿并不能真正控制它们,它不会像您比较 'a'new String('a') 并得出它们不相等那样搜索错误

    关于instanceof:并非总是这样,如果定义变量let a: SomeObjectType,表达式a instanceof SomeObjectType 将返回true。

    考虑这个例子:

    const obj: Object = {}
    Object.setPrototypeOf(obj, null)
    console.log(obj instanceof Object) // false
    

    甚至更简单:

    class A {
      foo: boolean
    }
    
    // this is fine, because { foo: false } has all properties of A
    let obj: A = { foo: false } 
    console.log(obj instanceof A) // false
    

    【讨论】:

    • 所以这就是 TypeScript 基本上回到纯 JavaScript 的地方。我希望它比那更聪明。您的第二个 sn-p 字面上转换为 let obj = { foo: false }。我希望它产生类似:let obj = Object.assign({new A(), { foo: false }),然后obj instanceof A 是真的。
    • 另外,这种行为有点违反this statement from the Handbookinstanceof 的右侧需要是一个构造函数,TypeScript 会缩小到 [...]函数的原型属性,如果它的类型不是any.
    • @noseratio,好吧,如果它被转换为Object.assign(blabla),这将改变代码的行为,这将违反打字稿的第五个非目标(github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals)——不要改变代码取决于类型系统。它似乎并没有违反你引用的短语。在 if 子句 obj 内写入 if(obj instanceof A) {...} 将被视为 A 的实例,但如果此 if 子句未触发,则 typescript 不会缩小任何看起来正确的行为
    • 也许你是对的。我对现代 JavaScript 相当流利,但我是 TypeScript 的新手。来自 C#,我的(不合理的?)期望 TypeScript 将有助于像这样的类型完整性,而不是默认使用 JS ?
    • @noseratio,好吧,祝你好运;)。 typescript 中的类型与 C 语言、Java 等中的类型有很大不同,所以也许你会适应这一点
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-31
    • 2014-02-15
    相关资源
    最近更新 更多