[译]<<Effective TypeScript>> 高效TypeScript62个技巧 技巧9-10

417 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 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, nullundefined, 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严格区分简单类型和对应的包装器:

  • string and String
  • number and Number
  • boolean and Boolean
  • symbol and Symbol
  • bigint and BigInt

实际上,常常会有人把两者弄混:

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"