写js很多年,刚开始写ts的时候,着实有些不习惯,整体感觉就像一个放荡不羁的少年被圈在了封闭的学校一样,后来用了一段时间发现,ts的确是学校里面一个严苛的教导主任,当你一旦不遵守校规的时候,就会让你叫家长(报错)。慢慢的你也就会成为一个乖巧懂事的好学生了,下面大家看看我怎么一步一步学好的哈
一、养成“先思考后动手”的好习惯
在以往的开发过程中,习惯总是“先想好一个大概,然后边做边想再边改”。这样的优势是执行快,顺利的时候效率会很高,但更多的时候会不断地推翻自己先前的想法,相信不少的人也有跟我类似的体会。
ts是js的超集。意思就是在ts中可以直接书写js。在我的第一感觉里,js就像是编译后的可执行文件,众所周知 ts 是强类型的语言,就像是Java语言。这也意味着它能有效制约开发者在开发过程中“随心所欲”的程度。下面详细介绍下👇
1. 定义类型方式和扩展
TypeScript中定义类型有两种方式:接口(interface)和类型别名(type alias)。在下面的例子中,除了语法不一样,定义的类型是一样的:
//interface
interface PointI {
x: number;
y: number;
}
interface SetPointI {
(x:number, y:number): void;
}
// or
//type alias
type PointT = {
x: number;
y: number;
}
type SetPointT = (x:number, y:number) => void;
- 接口和类型别名不仅均可以扩展,而且接口和类型别名并不互斥的,也就是说,接口可以扩展类型别名,类型别名也可以扩展接口
// interface extends type alias
type PointX = {
x: number;
}
interface PointY extends PointX {
y: number;
}
// type alias extends interface
interface Point2X{
x: number;
}
type Point2Y = Point2X & {y: number;}
- 接口和类型别名也有差别,比如一个接口可以定义多次,并将被视为一个接口,但是类型别名却不可以重复
接口和类型别名的选用时机:
- 在定义公共 API(如编辑一个库)时使用
interface,这样可以方便使用者继承接口; - 在定义组件属性(
Props)和状态(State)时,建议使用type,因为type的约束性更强; type类型不能二次编辑,而interface可以随时扩展。
2. TS 支持的 JS 新特性
2.1 可选链(Optional Chaining)
- TypeScript 3.7 实现了呼声最高的 ECMAScript 功能之一:可选链(Optional Chaining)。有了可选链后,我们编写代码时如果遇到
null或undefined就可以立即停止某些表达式的运行。可选链的核心是新的?.运算符.
obj?.prop
obj?.[expr]
arr?.[index]
func?.(args)
- 可选链(Optional Chaining)
?.是ES11(ES2020)新增的特性,可选链可以让我们在查询具有多层级的对象时,不再需要进行冗余的各种前置校验:
let age = user && user.info && user.info.getAge()
let age = user?.info?.getAge?.()
- 但需要注意的是,
?.与&&运算符行为略有不同,&&专门用于检测false值,比如空字符串、0、NaN、null 和 false 等。而?.只会验证对象是否为null或undefined,对于 0 或空字符串来说,并不会出现 “短路”。
2.2 可选属性
- 在面向对象语言中,接口是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类去实现。 TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。
在 TypeScript 中使用
interface关键字就可以声明一个接口:
interface Person {
name: string;
age: number;
}
let zhangsan: Person = {
name: "zhangsan",
age: 33,
};
- 上述代码中,我们声明了
Person接口,它包含了两个必填的属性name和age。在初始化 Person 类型变量时,如果缺少某个属性,TypeScript 编译器就会提示相应的错误信息,比如: - 为了解决上述的问题,我们可以把某个属性声明为可选的:
interface Person {
name: string;
age?: number;
}
let lisi: Person = {
name: "lisi"
}
2.3 空值合并运算符(Nullish coalescing Operator)
- 当空值合并运算符的左表达式不为
null或undefined时,不会对右表达式进行求值。
const goods = {
price: 0,
}
let goods1 = goods.price ?? '暂无报价'
let goods2 = goods.jdPrice ?? '暂无报价'
console.log(goods1)//0
console.log(goods2)//暂无报价
- 与逻辑或操作符(
||) 不同,||会在左侧操作数为false值(例如,''或0)时返回右侧操作数。也就是说,如果使用||来为某些变量设置默认值,可能会遇到意料之外的行为:
const goods = {
price: 0,
}
let goods1 = goods.price || '暂无报价'
let goods2 = goods.price ?? '暂无报价'
console.log(goods1)//暂无报价
console.log(goods2)//0
3.类型收窄
TypeScript类型收窄就是从宽类型转换成窄类型的过程,其常用于处理联合类型变量的场景。- 在
TypeScript中,有许多方法可以收窄变量的类型:- 类型断言
- 类型守卫
- 双重断言
3.1 类型断言
- 类型断言有两种:
值 as 类型or<类型>值一般我们统一使用值 as 类型这样的语法,因为<>容易跟泛型语法起冲突。 - 当
TypeScript不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性或方法
interface rabbit {
name: string;
jump(): void;
}
interface dog {
name: string;
run(): void;
}
function isRabbit1(animal: rabbit | dog){
return animal.name
}
- 而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法,如:
- 上图的例子中,获取
animal.jump的时候会报错。此时可以使用类型断言,将animal断言成rabbit类型,就可以解决访问animal.jump时报错的问题:
interface rabbit {
name: string;
jump(): void;
}
interface dog {
name: string;
run(): void;
}
function isRabbit(animal: rabbit | dog){
if(typeof (animal as rabbit).jump === 'function'){
return true
}
return false
}
- 类型断言虽好,但是不能滥用,滥用之后可能会绕过编译器,但是无法避免运行时的错误。
interface rabbit {
name: string;
jump(): void;
}
interface dog {
name: string;
run(): void;
}
function rabbitJump(animal: rabbit | dog)
{
(animal as rabbit).jump()
}
const jack: dog = {
name: 'Jack',
run() {
console.log("跑")
}
}
rabbitJump(jack)//运行会报错
-
TypeScript编译器信任了我们的断言,故在调用rabbitJump()时没有编译错误,但由于jack上并没有jump方法,就会导致在运行时发生错误。 -
使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。
3.2 类型守卫
-
类型守卫主要有以下几种方式:
- typeof:用于判断
number,string,boolean或symbol四种类型; - instanceof:用于判断一个实例是否属于某个类
- in:用于判断一个属性/方法是否属于某个对象
- typeof:用于判断
-
可以利用
typeof实现类型收窄和never类型的特性做全面性检查,如下面的代码所示:
type Foo = string | number
function test(input: Foo) {
if (typeof input == 'string') {
// 这里 input 的类型「收紧」为 string
} else if (typeof input == 'number') {
// 这里 input 的类型「收紧」为 number
} else {
// 这里 input 的类型「收紧」为 never
const isShow: never = input
}
}
-
可以看到,在最后的
else分支里面,我们把收窄为never的input赋值给一个显示声明的never变量,如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事修改了Foo的类型,并且忘记了修改test方法里面控制流的时候: -
这时候
else分支的input类型会被收窄为boolean类型,导致无法赋值给never类型,这时就会产生一个编译错误。 -
使用
instanceof运算符收窄变量的类型 -
使用
in做属性检查
interface Foo {
foo: string;
}
interface Bar {
bar: string;
}
function test(input: Foo | Bar) {
if ('foo' in input) {
// 这里 input 的类型「收紧」为 Foo
} else {
// 这里 input 的类型「收紧」为 Bar
}
}
3.3 双重断言
- 类型断言并不总是能成功,比如:
- 如上例子中的代码将会报错(类型 "Event" 到类型 "HTMLElement" 的转换可能是错误的,因为两种类型不能充分重叠。如果这是有意的...),尽管已经使用了类型断言。
- 如果你仍然想使用那个类型,你可以使用双重断言。首先断言成兼容所有类型的
any,编译器将不会报错:
function handler(event: Event) {
const element = (event as any) as HTMLElement; // OK
}
- TypeScript 是怎么确定单个断言是否足够 ? 当
S类型是T类型的子集,或者T类型是S类型的子集时,S能被成功断言成T。这是为了在进行类型断言时提供额外的安全性,完全毫无根据的断言是危险的,如果你想这么做,你可以使用any。
4.枚举
- 枚举是组织收集有关联变量的一种方式,首先我们看看普通枚举,如果你使用过其它编程语言应该会很熟悉。
enum Color {
Red = 1, //1
Green, //2
Blue, //3
}
- 如上,我们定义了一个数字枚举,
Red使用初始化为1。 其余的成员会从1开始自动增长。 换句话说,Color.Red的值为1,Green为2,Blue为3,Red要是没有初始化,即从0开始。 但是这样写就有问题了: - 当枚举某个成员非数字时,下面增长的枚举成员需要和其保持一致,这样方能避免报错
enum Color {
Red, // 0
Green = "Green",// Green
Blue = "Blue",// Blue
}
- 普通枚举的值不会在编译阶段计算,而是保留到程序的执行阶段,我们看看下面的例子:
enum Color {
//常量枚举
Red,
purple,
Green = Color.Red,
Blue = 1 + 1,
//非常量枚举
yellow = Math.random(),
black = 'hello'.length,
}
- 上例的编译结果是:
var Color;
(function(Color) {
Color[(Color['Red'] = 0)] = 'Red';
Color[(Color['purple'] = 0)] = 'purple';
Color[(Color['Green'] = 0)] = 'Green';
Color[(Color['Blue'] = 2)] = 'Blue';
Color[(Color['yellow'] = Math.random())] = 'yellow';
Color[(Color['black'] = 'hello'.length)] = 'black';
})(Color || (Color = {}));
- 先让我们聚焦
Color[(Color['Red'] = 0)] = 'Red''这行代码,其中Color['Red'] = 0的意思是将Color对象里的Red成员值设置为0。注意,JavaScript 赋值运算符返回的值是被赋予的值(在此例子中是0),因此下一次 JavaScript 运行时执行的代码是Color[0] = 'Red'。意味着你可以使用Color变量来把字符串枚举类型改造成一个数字或者是数字类型的枚举类型,如下所示:
enum Color {
Red,
Blue = 1 + 1,
yellow = Math.random(),
black = 'hello'.length,
}
console.log(Color[0]); // 'Red'
console.log(Color['Red']); // 0
console.log(Color[Color.Red]); // 'Red'
5. 高级类型
- 除了
string、number、boolean这种基础类型外,我们还应该了解一些类型声明中的一些高级用法。
5.1 extends 关键字
- 基础用法:
T extends U ? X : Y
- 表示,如果 T 可以赋值给 U (类型兼容),则返回 X,否则返回 Y;以内置的泛型接口
Extract为例,它的实现如下:
type Extract<T, U> = T extends U ? T : never
TypeScript将使用never类型来表示不应该存在的状态。上面的意思是,如果 T 中的类型在 U 存在,则返回,否则抛弃。- 假设我们两个类,有三个公共的属性,可以通过
Extract提取这三个公共属性: TypeScript中内置了很多工具泛型,除了介绍的这些,内置的泛型在TypeScript内置的lib.es5.d.ts中都有定义,所以不需要任何依赖就可以直接使用。
5.2 使用 keyof
- 基础实例:
interface Foo {
name: string;
age: number
}
type T = keyof Foo
//等同于
type T = "name" | "age"
extends经常与keyof一起使用,例如我们有一个getValue方法专门用来获取对象的值,但是这个对象并不确定,我们就可以使用extends和keyof进行约束:- 当传入对象没有的
key时,编辑器则会报错。
5.3 使用 in
in则可以遍历枚举类型, 例如:
type Keys = "a" | "b"
type Obj = {
[p in Keys]: any
} // { a: any, b: any }
keyof产生联合类型,in则可以遍历枚举类型, 所以他们经常一起使用。
二、好的习惯都需要慢慢养成
如果只是掌握了 TypeScript 的一些基础类型,可能很难游刃有余的去使用 TypeScript。想要很好的驾驭它,只能不断的学习和掌握它。
本文只是介绍了几种常用的实践,希望还没接触 TypeScript 或对 TypeScript 还不太熟悉的小伙伴赶快在项目实践起来,努力提升代码可维护性和开发幸福感,感谢大家的阅读🙏