是否存在类型断言和类型注释在 TypeScript 中等效的情况



通常不建议使用类型断言,并且被认为是有害的。

对于复杂类型,例如这个Person类型,我们可能会遇到person2对象的问题,因为我们可能希望它有一个age属性,而它没有(但person1正确地给了我们一个 TypeScript 错误):

type Person = { name: string, age: number };
// type annotation
const person1: Person = { name: 'Victoria' };
// type assertion
const person2 = { name: 'Victoria' } as Person;

但我想知道是否存在类型断言和类型注释之间没有区别的情况。

在以下三种情况下,类型断言和类型注释之间有区别吗?和/或还有其他情况吗?

  1. 对于像string这样的基元类型,number

    // type annotation
    const val1: string = 'foo';
    // type assertion
    const val2 = 'foo' as string;
    
  2. 对于基元类型的数组,如string[]number[]

    // type annotation
    const values1: string[] = ['foo', 'bar'];
    // type assertion
    const values2 = ['foo', 'bar'] as string;
    
  3. anyany[]的情况下

    // type annotation
    const val1: any = 'foo';
    const values1: any[] = ['foo', 1];
    // type assertion
    const val2 = 'foo' as any;
    const values2 = ['foo', 1] as any[];
    

这可能是一个意见问题,因为它实际上取决于你所说的"等效"是什么意思。 如果您没有发生事故,不系安全带的汽车是否"等同于"骑安全带?


Type 注释通常只允许您将某种类型的值A分配给某种类型的变量B其中A extends B. 我们说A可以分配给B,虽然有一些例外和繁琐的细节,但你可以认为AB的一个子类型,或者AB相同或更窄。 这样的赋值在 TypeScript 中相对安全(尽管由于 TypeScript 的类型系统并不完全健全,它们仍然允许一些不安全的事情)。

所以let foo: string | number = "hello"工作是因为您将文字类型的值"hello"(或者可能是string类型的值)分配给string | number类型的变量,而前者是后者的子类型。 您也可以编写let bar: 123 = 123因为文本类型123是自身的子类型(子类型不需要是"正确"或"严格"的子类型)。 但是你不能写let baz: string = Math.random() < 0.5 ? "baz" : 123;没有错误。"baz" | 123string | number类型不是string的子类型。

另一方面,

Type 断言允许您将某种类型的值A分配给某种类型的变量BA extends BB extends A的位置。 (同样,我在这里掩盖了一些复杂性)。 编译器所关心的只是它将AB视为彼此"相关"。 因此,只有当您尝试进行两个类型不相关的赋值(如let foo = "hello" as number)时,才会出现错误,并且您始终可以通过首先断言中间相关类型(如let bar = "hello" as unknown as number)来解决此问题。

无论如何,将A扩大BA extends B方向相对安全,其作用类似于类型批注,但A缩小BB extends A是不安全的。 这种缩小断言只应在你真的希望编译器将它认为是A的值视为B类型的值的情况下使用,即使它无法验证这一点。 但你现在不是在谈论"不安全"的方向。

所以注释和扩大断言应该大多是"等价的",对吧?


嗯,当然,有点,只要什么都没有改变。 类型注释为您做的一件事是,如果代码稍后更改并且分配的值突然违反批注类型,则提供警告:

const myValue = "foo";
// ... intervening code
const val1: string = myValue; // okay
const val2 = myValue as string; // no error
val2.toUpperCase(); 

如果稍后有人出现并编辑代码,以便

const myValue = Math.random() < 0.5 ? "foo" : 123;

然后注释将在编译时捕获问题:

const val1: string = myValue; // error

但是这个断言很高兴地让你继续没有问题,直到运行时:

const val2 = myValue as string; // no error!
val2.toUpperCase(); // 50% chance of runtime error 

这就是为什么我认为类型断言是取下安全带(也许一个断言只是松开它,而做双重a as unknown as B是完全取下它)。 一切都很好...直到它不是。 如果您提前 100% 确定您不会发生车祸,那么就没有区别。 (但你真的知道这一点吗?

如果你确定你的断言是扩大而不是缩小,那么它主要是"等同于"一个注释(但你真的确定这一点吗? 不过,总的来说,我建议使用注释而不是扩大断言。


我能想到的一种情况是,扩大断言不等同于注释。A类型的值分配给B类型的变量(其中B是联合类型)时,编译器将使用控制流分析来暂时缩小B表观类型:

let foo: string | number = "foo";
foo.toUpperCase(); // no error, foo has been temporarily narrowed to string
foo = 456; // no error, foo was annotated as string | number
foo.toFixed(); // no error, foo was reset and has been temporarily narrowed to number

但是断言不是这样工作的:

let bar = "foo" as string | number;
bar.toUpperCase(); // error! bar was never narrowed
bar = 456 as string | number;
bar.toFixed(); // error!

很多时候,您更喜欢控制流缩小行为,因此您仍然需要注释而不是断言。 但是,有时编译器会比您希望的更激进:

let baz: string | number = "baz" // this might be a number sometime but for testing it's "baz"
if (typeof baz === "number") {
baz.toFixed(); // error?! toFixed() doesn't exist on type 'never'
}

在这里,我将baz设置为"baz",但我更希望编译器将其视为string | number。 因为它注意到它只是string,它非常沮丧,我可能会测试它是number然后把它当作一个。 扩大断言可以处理如下情况:

let qux = "qux" as string | number;
if (typeof qux === "number") {
qux.toFixed(); // okay
}
>游乐场链接到代码

最新更新