通常不建议使用类型断言,并且被认为是有害的。
对于复杂类型,例如这个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;
但我想知道是否存在类型断言和类型注释之间没有区别的情况。
在以下三种情况下,类型断言和类型注释之间有区别吗?和/或还有其他情况吗?
对于像
string
这样的基元类型,number
?// type annotation const val1: string = 'foo'; // type assertion const val2 = 'foo' as string;
对于基元类型的数组,如
string[]
,number[]
?// type annotation const values1: string[] = ['foo', 'bar']; // type assertion const values2 = ['foo', 'bar'] as string;
在
any
或any[]
的情况下// 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
,虽然有一些例外和繁琐的细节,但你可以认为A
是B
的一个子类型,或者A
与B
相同或更窄。 这样的赋值在 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" | 123
或string | number
类型不是string
的子类型。
Type 断言允许您将某种类型的值A
分配给某种类型的变量B
A extends B
或B extends A
的位置。 (同样,我在这里掩盖了一些复杂性)。 编译器所关心的只是它将A
和B
视为彼此"相关"。 因此,只有当您尝试进行两个类型不相关的赋值(如let foo = "hello" as number
)时,才会出现错误,并且您始终可以通过首先断言中间相关类型(如let bar = "hello" as unknown as number
)来解决此问题。
无论如何,将A
扩大到B
的A extends B
方向相对安全,其作用类似于类型批注,但A
缩小到B
B 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
}
>游乐场链接到代码