一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情。
本文的翻译于<<Effective TypeScript>>, 特别感谢!! ps: 本文会用简洁, 易懂的语言描述原书的所有要点. 如果能看懂这文章,将节省许多阅读时间. 如果看不懂,务必给我留言, 我回去修改.
技巧9: 尽量使用类型声明而不是断言
一. 为啥尽量用类型申明代替断言
ts似乎有两种方法为变量赋值, 并指定类型:
interface Person { name: string };
const alice: Person = { name: 'Alice' }; // Type is Person
const bob = { name: 'Bob' } as Person; // Type is Person
但是这两种方法不一样!
- 第一种是类型声明: 告诉ts, alice 是Person类型,赋值的时候必须满足Person类型的结构
- 第二种是断言: 不管ts自己推倒出bob的类型是什么, 我知道的更多, 很可能是Person类型 两者有什么不一样? 例如:
const alice: Person = {};
// ~~~~~ Property 'name' is missing in type '{}'
// but required in type 'Person'
const bob = {} as Person; // No error
类型申明会检查值是否满足接口, 这里不满足就报错了.
断言不报错; 因为你告诉ts, 不管什么原因, 我知道的比ts更多.
这样类似的情况发生在指定其他属性的时候:
const alice: Person = {
name: 'Alice',
occupation: 'TypeScript developer'
// ~~~~~~~~~ Object literal may only specify known properties
// and 'occupation' does not exist in type 'Person'
};
const bob = {
name: 'Bob',
occupation: 'JavaScript developer'
} as Person; // No error
所以除非有特殊情况, 请尽量使用类型声明而不是断言
你可能会看到这样的代码:
const bob = <Person>{}, 这是早期ts版本的断言写法, 效果等同于{} as Person, 第一种写法被逐渐淘汰的原因: <Person> 容易被误认为html标签
二. 如何在箭头函数中使用类型声明.
部分人不是很清楚如何在箭头函数中使用类型声明. 以下面这段代码为例, 我该如何指定people的类型为Person[]?
const people = ['alice', 'bob', 'jan'].map(name => ({name}));
// { name: string; }[]... but we want Person[]
有人会使用断言来做:
const people = ['alice', 'bob', 'jan'].map(
name => ({name} as Person)
); // Type is Person[]
但是依旧会遇到我们刚刚说过的问题:
const people = ['alice', 'bob', 'jan'].map(name => ({} as Person));
// No error
最直接的方法, 返回Person类型:
const people = ['alice', 'bob', 'jan'].map(name => {
const person: Person = {name};
return person
}); // Type is Person[]
存在一种更简洁的方法, 在箭头函数中指定返回值类型:
const people = ['alice', 'bob', 'jan'].map(
(name): Person => ({name})
); // Type is Person[]
记住上面的写法不要和另一种写法混淆:(name: Person). 后者表示name是Person类型的.而name是string类型, 显然会报错. 保险起见,我们可以写明最终值的类型:
const people: Person[] = ['alice', 'bob', 'jan'].map(
(name): Person => ({name})
);
在两处都写明类型申明是有意义的, 当我们的代码更多, 越早写类型声明, 容易越早发现类型错误.
三. 我们该何时使用断言
当我们比ts知道的更多上下文的时候, 使用断言才有意义. 比如: 你可能比ts更精确的知道DOM元素.
document.querySelector('#myButton').addEventListener('click', e => {
e.currentTarget // Type is EventTarget
const button = e.currentTarget as HTMLButtonElement;
button // Type is HTMLButtonElement
});
因为ts无法获取你page上的dom元素.所以不知道#myButton就是button
还有你可能会遇到非空断言:
const elNull = document.getElementById('foo'); // Type is HTMLElement | null
const el = document.getElementById('foo')!; // Type is HTMLElement
! 作为前缀,是js用作boolean变量取反的操作符. 作为后缀, 是表示非空操作符.
这个非空操作符, 在ts编译js代码中会被擦除. 你需要100%确定使用的变量不是null, 否则你还是使用条件表达式来检查是否为空.
断言也不是在任意对象之间进行转换的. 比如A和B, 只有当一个是另一个的子集, 才能用断言在A和B之间进行转换 .例如HTMLElement 是 HTMLElement | null 的子集, 所以可以使用断言转换.
但是你不能在非子集关系的类型之间进行转换:
interface Person { name: string; }
const body = document.body;
const el = body as Person;
// ~~~~~~~~~~~~~~ Conversion of type 'HTMLElement' to type 'Person'
// may be a mistake because neither type sufficiently
// overlaps with the other. If this was intentional,
// convert the expression to 'unknown' first
任何集合都是 unknown类型的子集, 上面的可以通过下面的方法进行转换:
const el = document.body as unknown as Person; // OK
技巧10: 避免使用对象包装器 (String, Number, Boolean, Symbol, BigInt)
除了对象object, js还有7种简单类型:strings, numbers, booleans, null, undefined, symbol, and bigint. 除了前五种一开始就有, symbol类型在ES2015中添加进来的, bigint正在被最终确定.
简单类型和对象object最大区别在于:不可变和没有方法. 但是你依旧可以执行:
> 'primitive'.charAt(3)
"m"
原因在于js偷偷做了一些微妙的事情. 尽管简单类型没有方法, 但是js定义了一个对象String作为类. 当你获取'primitive'的charAt方法, js就会用String将'primitive'进行包装, 再调用方法. 调用完成后将包装丢弃.
你可以通过String.prototype观察上述过程:
// Don't do this!
const originalCharAt = String.prototype.charAt;
String.prototype.charAt = function(pos) {
console.log(this, typeof this, pos);
return originalCharAt.call(this, pos);
};
console.log('primitive'.charAt(3));
会输出:
[String: 'primitive'] 'object' 3
m
这里的this指的是String包装器. 当你直接对String包装器进行实例化. String包装器有的时候像简单类型, 有的时候不像.比如String只等于他自己:
> "hello" === new String("hello")
false
> new String("hello") === new String("hello")
false
这样的隐式转换解释了一个奇怪的现象:
> x = "hello"
> x.language = 'English'
'English'
> x.language
undefined
当x被转换成String实例, language属性被放到了String实例上面.随后String实例马上有被扔掉了.
同样的:其他的简单类型也存在类似的转换:Number 和 number, Boolean 和 boolean , Symbol 和 symbol,BigInt 和bigints
这些包装器类型是为了给简单类型提供方法, 一般不会直接去实例化包装器.
ts严格区分简单类型和对应的包装器:
stringandStringnumberandNumberbooleanandBooleansymbolandSymbolbigintandBigInt
实际上,常常会有人把两者弄混:
function getStringLen(foo: String) {
return foo.length;
}
初始化不会报错:
getStringLen("hello"); // OK
getStringLen(new String("hello")); // OK
但是如果你期待一个string, 但是传入一个String就会报错.
function isGreeting(phrase: String) {
return [
'hello',
'good day'
].includes(phrase);
// ~~~~~~
// Argument of type 'String' is not assignable to parameter
// of type 'string'.
// 'string' is a primitive, but 'String' is a wrapper object;
// prefer using 'string' when possible
}
所以,string可以指定给String, 但是String不能指定给string. 是不是感到有些困惑?没关系, 要记得一点: 尽量使用string!
也有人会这样写错:
const s: String = "primitive";
const n: Number = 12;
const b: Boolean = true;
虽然说这些变量到了js运行时, 都是简单对象. 但是在ts类型检查中容易出错, 同时这样声明容易让人误解.
最后说一点: 我们可以不用new直接调用BigInt, Symbol, 返回的值是简单类型:
> typeof BigInt(1234)
"bigint"
> typeof Symbol('sym')
"symbol"