TS 基础中的基础
对于前端小伙伴来说,TypeScript 肯定都不陌生,但本人之前一直对 TS 了解的不多,这次决定全面学习一下 TS 并总结成博客文章
废话不多说,咱直接就开始吧 👊
TypeScript 概览
TypeScript 是什么?
简单理解就是 TypeScript 是增加了类型约束的 JavaScript,并且可以被编译成原生 JavaScript。
为什么需要 TypeScript?
a. 与弱类型的 JS 结合,在编译期间增强类型检查,提前发现可能的缺陷
b. 通过强类型约束可以放心地进行多人协作开发,保证项目的可维护性
c. 与代码编辑器集成,提供自动补全、引用跳转等实用功能,提升开发效率
基本用法
下面来看看 TypeScript 的基本用法
基本类型
简单类型介绍
对于简单类型呢,就是 string、number、boolean、symbol、undefined 和 null,比较基础:
1 | const str: string = 'hello'; |
自动推断类型
在某些场景,ts 是可以自己推断出类型的,比如:
- 初始化赋值的时候
1 | let myName = 'Daniel Yang'; |
duang~ ts 发出了报错:
👇
- 对函数的返回值
1 | function greet(name: string) { |
ts 会自动推断出返回值类型:
- 存在比较明显的上下文推断
1 | const arr = [1, 2, 3]; |
在 map 方法中 ts 能推断出遍历元素的类型:
在这些场景下由于 ts 能推断出具体类型,所以是可以省略类型注释的,还能减少代码的长度😬
特别的类型
下面介绍一些特别的类型
1. any
在 ts 里 有一个很特殊的 any
类型,对于不知道具体类型 或者就是不想写类型的情况,可以使用 any
来声明
不过这样会导致 ts 对该变量禁用检查,丢失掉 ts 该有的作用,所以需要避免过度使用 any
2. unknown
unknown
代表着任意的值,它和 any
非常像,但由于对 unknown 进行任意操作都是不合法的,所以它比直接使用 any
更安全
1 | function fnWithAny(a: any) { |
3. never
never
意味着永远不会发生,就像那年秋天,咳咳,扯远了。。。🤷♂️
对于抛出异常会提前终止执行的函数来说,适合对其返回类型声明为 never:
1 | function fail(): never { |
看起来好像没啥用🤐
但其实 never
非常适合用于防止对联合类型有遗漏使用的情况,例如:
1 | type Shape = 'circle' | 'square'; |
如果有一天大家对 Shape 增加了新类型 star
,但是忘记去新增 switch 的 case 分支,此时 default 分支里 ts 会报错导致代码编译不通过,将这个遗漏 case 分支的隐患暴露出来!
绝
4. void
void
意味着函数没有返回值或不返回任何明确的值:
1 | function noop1(): void { |
复杂类型
接下来咱们来看下如何在 ts 里给复杂对象添加类型声明
首先来认识一下 type
和 interface
关键字
1. type 类型别名
在 ts 里,我们可以使用 type
关键词来给任意类型添加命名,这样可以方便引用和复用:
1 | // 添加 Point 的类型别名 |
同时我们可以使用 &
符号将多个 type 进行组合:
1 | type Animal = { |
2. interface 接口类型
interface
是另一种用来声明对象类型的方式:
1 | interface Point { |
我们可以使用 extends
关键字对 interface 进行继承:
1 | interface Animal { |
既然有两种类型声明的方式,那么问题来了,type
和 interface
有啥区别呢?🤔
type 和 interface 的区别
type
和 interface
主要有以下几个区别:
interface 只能声明对象类型,但 type 除了对象类型以外,还可以声明简单类型和 union 联合类型
1
2
3
4
5
6
7
8
9
10
11
12
13// 对象类型
interface Info {
name: string;
desc: string;
}
type Info = {
name: string;
desc: string;
}
// type 还可以声明简单类型和联合类型
type name = string;
type value = string | number;interface 的重复声明可以合并,然而 type 不能重复声明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// interface 可以重复声明,声明的属性会进行合并
interface Info {
name: string;
}
interface Info {
desc: string;
}
type Info = {
name: string;
}
type Info = { // ❌ Error: type 不能重复声明
desc: string;
}type 和 interface 实现类型扩展的方式不同
type 通过
&
符号进行类型合并,而 interface 通过extends
关键词实现继承1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19interface A {
a: string;
}
interface B extends A {
b: number;
}
🤗 // interface B => { a: string; b: number; }
type A = {
a: string;
}
type B = A & {
b: number;
}
🤗 // type B => { a: string; b: number; }
3. 对象
讲完了类型声明的方式,我们来看看在 ts 里如何对对象进行类型声明,如下所示👇:
1 | interface Info { |
同时我们可以用 ?
和 readonly
修饰符来修饰对象属性:
?
是可选修饰符,意味着该属性可以不赋值1
2
3
4type Info = {
name: string;
phone?: string; // phone => string | undefined
}readonly
是只读修饰符,表示该属性初始化后不能再次修改1
2
3
4
5
6
7
8type Info = {
readonly name: string;
}
let info: Info = {
name: 'Daniel'
}
info.name = 'Tom'; // ❌ Error: Cannot assign to 'name' because it is a read-only property.
在使用可选属性前需要检查属性是否存在,否则 ts 会产生报错提示:
1 | function printName(obj: { first: string, last?: string }) { |
对于 readonly
来说虽然不会真的改变属性的性质,但会在编译期的类型检查期间禁止属性的重新写入:
1 | function doSomething(obj: { readonly message: string }) { |
readonly
修饰符与 const
声明挺类似的,它并不意味着属性的值完全不能修改,而是指不能再重新更新属性的引用:
1 | type PersonalInfo = { |
4. 数组
对数组来说,它的类型声明有两种方式,以字符串数组为例:
string[]
Array<string>
这两种写法的结果没有区别,只是第二种是泛型 U<T>
的写法,我们稍后再详细介绍泛型😌
与对象属性一样,我们也可以将数组声明为只读数组,同样有两种方式:
ReadonlyArray<string>
readonly string[]
这样使得数组内容不可更改:
1 | const arr: readonly string[] = ['apple', 'banana']; |
5. 函数
对函数来说,需要声明类型的地方有 函数参数
和 函数返回值
,例如:
1 | function getMax(a: number, b: number): string { |
同样地,我们也可以声明可选参数和只读参数:
1 | function fixed(n: number, digit?: number) { |
常用类型
接下来介绍几种常用的类型
union 联合类型
联合类型是将两个以上的类型组合起来的形式,表示某个值可以是其中任意一个类型:
1 | function printId(id: number | string) { |
除了类型联合外,咱们还可以联合具体的值,这样在代码编辑器里还能方便地增加提示:
1 | function printText(s: string, alignment: 'left' | 'right' | 'center') {} |
💣 需要注意的是,在 ts 里使用联合类型时,只有当某个属性是所有类型所共有的才可以直接用
比如某个联合类型是 string | number
,如果直接使用只存在于 string
类型上的属性和方法是会喜提报错的 🙃:
1 | function print(val: string | number) { |
咱就是说在使用某一类型特有的属性之前,需要通过明确的类型判断让 ts 知道变量具体的类型,这样就能正常使用类型所对应的属性和方法了
偶总结了下 => 至少有以下 几种方式 可以用来更明确地判断变量的类型:
使用
typeof
操作符1
2
3
4
5
6function padLeft(padding: number | string, input: string) {
if (typeof padding === 'number') { // 使用 typeof 明确变量的类型
return ''.repeat(padding) + input;
}
return `${padding}${input}`;
}使用
in
操作符1
2
3
4
5
6
7
8
9
10type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ('swim' in animal) { // 检查 swim 是否存在于 animal 原型链上,即是否为 Fish 类型
animal.swim();
} else {
animal.fly();
}
}使用
instanceof
操作符1
2
3
4
5
6
7function logValue(x: Date | string) {
if (x instanceof Date) { // 是否为 Date 类型实例
console.log(x.toUTCString());
} else {
console.log(x);
}
}使用自定义类型预测方法
除了使用 JS 本身的语言能力来做,咱也可以自定义一些类型判断方法
比如我们需要判断一个变量究竟是
Fish
类型还是Bird
类型,可以这样写:1
2
3function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined; // 验证下 pet 变量上是否存在 swim 属性
}然后放在条件判断里就好了:
1
2
3
4
5if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
enum 枚举
enum 枚举是 ts 在 js 语法之外新增的特性,它允许咱们定义一组命名常量,比如:
1 | enum NumericDirection { |
简单来说就是:
- 数字类型的枚举默认值为 0,后面的成员如果没有赋值则继续累加 1
- 字符类型的枚举必须要赋值
枚举成员也可以是混合类型,例如这样:
1 | enum MixedType { |
比较有意思的是枚举其实是真实的对象,所以在代码里可以作为值直接使用:
1 | enum Response { |
如果是这样,那问题又来了: ts 的枚举和 js 的对象有什么区别呢?
👻
emm… 枚举与对象主要有两点不同:
数字类型的枚举会生成
反向映射
,可以通过枚举的值获取到对应的键 key:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15enum NumericEnum {
LEFT = 1,
RIGHT = 2,
}
NumericEnum[NumericEnum.LEFT]; // 'LEFT'
NumericEnum[1]; // 'LEFT'
// 让我们打印下 NumericEnum 的 key
for (const key of Object.keys(NumericEnum)) console.log(key)
// '1'
// '2'
// 'LEFT'
// 'RIGHT'
// 。。。不是很明白为什么要这样设计?🤷♂️枚举成员是只读类型
1
NumericEnum['LEFT'] = 3; // ❌ Error: Cannot assign to 'LEFT' because it is a read-only property.
Tuple 元组
介绍完枚举,我们来认识下 Tuple 元组
这名字听起来很高大上,但其实。。。它就是数组而已
不过在元组里可以混合着不同类型,比如: pair: [string, number]
这样子,它就属于元组
由于元组一般是知道元素数量和对应的类型,所以 ts 可以对元组的下标访问是否越界和具体元素的操作是否合法做检查:
1 | function doSomething(pair: [string, number]) { |
为什么说是一般呢,因为元组里可以有可选元素和扩展元素,它们会造成元组的实际长度不确定
可选元素:咱可以在元素类型后面增加 ? 表示其为可选元素,需要注意可选元素只能出现在队尾
1
2
3type TupleArray = [number, string, boolean?];
const arr1: TupleArray = [1, '2']; // ✅ OK.
const arr2: TupleArray = [1, '2', true]; // ✅ OK.扩展元素:和 js 语法一样,咱可以用在类型前添加 … 表示它是一个扩展元素:
1
2
3type StringNumberBooleans = [string, number, ...boolean[]]; // 表示前两个元素分别是字符和数字类型,剩下的元素都是布尔类型
type StringBooleansNumber = [string, ...boolean[], number]; // 表示第一个和最后一个元素分别是字符和数字类型,中间的元素都是布尔类型
type BooleansStringNumber = [...boolean[], string, number]; // 表示最后两个元素分别是字符和数字类型,前面的元素都是布尔类型
进阶用法
恭喜你,能看到这里的人都是大佬,下面让我们来学一些 ts 的进阶用法😎
函数
函数重载
如果某个函数能够以不同的参数数量和参数类型来调用,那在 ts 里该如何对该函数进行类型声明呢?
答案是 => 我们可以 定义多个函数签名
比如我们要写一个展示日期的方法,该方法可以接收一个数字类型的时间戳参数
或 具体年、月、日三个参数
,那么可以这样写函数的类型声明:
1 | // 函数签名 |
⚠️不过需要强调的是,如果能用 union 联合类型声明的,就不要用重载来声明,否则会把简单问题复杂化
比如我们需要写一个返回字符串或数组长度的方法,假设使用重载来进行类型声明:
1 | // 函数签名 |
在普通调用下没有问题:
1 | len('hello'); // ✅ OK. |
但如果像下面这样调用,ts 就会报错:
1 | len(Math.random() > 0.5 ? 'hello' : [1, 2, 3]); // ❌ Error: |
因为此时参数类型在编译时没法确定,不能单独匹配任意一个函数签名:
但冷静下来想一想 🤔,在这种参数数量和返回值类型都相同的情况下,直接使用 union 联合类型不香吗:
1 | function len(x: string | any[]): number { |
函数泛型
下面我们来了解下 ts 里一个比较重要的概念: 泛型
泛型是用来描述同一类型在多个值之间的关联性🌟
比如某个方法需要返回数组参数的第一个元素,虽然可以像这样写类型声明:
1 | function getFirstElement(arr: any[]) { |
但这样会导致方法的返回值是 any
类型,有点简单粗暴,表达不了返回值和入参的关系
如果返回值的类型能明确地与入参数组的元素类型关联上就好了😌
此时我们就可以使用 泛型 来满足这个需求,如下:
1 | function getFirstElement<Type>(arr: Type[]): Type { |
See? ! 通过在函数签名处添加一个类型参数 Type
并用在参数列表和返回值声明里,我们就在它们俩之间建立了联系
现在当我们调用函数时,返回值的类型将会与数组元素的类型一致:
demo.png
Great!🥳
同时我们还可以使用 extends
关键字 对泛型增加限制
比如我们需要实现一个 在两元素中返回 length 属性最大的那个元素
方法:
1 | function getLonger<Type extends { length: number }>(a: Type, b: Type): Type { |
这样就限制了该泛型必须具有 number 类型的 length 属性:
1 | getLonger(10, 20); // ❌ Error: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'. |
对象
索引签名 index signature
在实际项目中会存在这样一种情况: 咱不知道一个类型里所有的属性值,但巧的是咱知道属性 key 和对应值的类型
此时就可以用索引签名来进行类型声明
比如可以这样声明一个下标是数字、值是字符串的对象:
1 | interface StringArray { |
But 只有 string
、number
和 symbol
可以用作对象 key 的类型,这也符合 JS 语言中对象 key 类型的范围
如果对象的属性有不同类型,我们可以用 union 联合类型来声明值的类型:
1 | interface NumberOrStringDic { |
最后,我们也可以给索引签名增加 readonly 前缀来防止属性被重新赋值:
1 | interface ReadonlyStringArray { |
对象泛型
与函数一样,对象也存在泛型声明 🤪
假设有这样一个对象 Box,它有一个包含任意类型的 content 属性,讲道理我们可以这样声明:
1 | interface Box { |
这没有问题,但使用 any
会导致 ts 对 content
属性移除了类型检查,比如:
1 | const box: Box = { |
在这种情况下我们就可以对 Box 对象进行泛型声明
可以这样理解下面的声明: Box 的 Type 就是 content 属性的类型
1 | interface Box<Type> { |
然后重点是 我们在引用 Box 类型的时候需要给出 Type 的具体类型,例如:
1 | const box: Box<string> = { |
这样 ts 会明确知道 box.content
是 string
类型,从对 box.content
的调用做出准确的检查: 🤗
另外我们还可以用 type
来声明泛型:
1 | type Box<Type> = { |
同时因为 type
不仅可以声明对象类型,我们还能用 type
来声明一些泛型的辅助类型,例如:
1 | type OrNull<T> = T | null; |
实用工具类型
文章的最后咱们来认识一些实用的工具类型吧 🔨
1. Partial
返回一个与 Type 属性相同但全被设为可选的新类型:
1 | interface Todo { |
2. Required
与 Partial 相反,返回一个与 Type 属性相同但全被设为必填的新类型:
1 | interface Info { |
3. Pick<Type, Keys>
从 Type 里挑出指定的 Keys 来构造一个新类型:
1 | interface Todo { |
4. Omit<Type, Keys>
与 Pick 相反,从 Type 里移除掉指定的 Keys 来构造一个新类型:
1 | interface Todo { |
5. Extract<UnionType, ExtractedMembers>
取 UnionType 和 ExtractedMembers 的交集来构造一个新类型:
1 | type T0 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // T0: 'a' |
6. Exclude<UnionType, ExcludedMembers>
从 UnionType 里移除掉 ExtractedMembers 存在的类型来构造一个新类型:
1 | type T0 = Exclude<'a' | 'b' | 'c', 'a'>; // T0: 'b' | 'c' |
7. NonNullable
从 Type 里移除掉 undefined
和 null
来构造一个新类型:
1 | type T0 = NonNullable<string | number | undefined | null>; // T0: string | number |