TypeScript 重学
TypeScript 编译
TypeScript 官方没有做运行环境,只提供编译器。编译时,会将类型声明和类型相关的代码全部删除,只留下能运行的 JavaScript 代码,并且不会改变 JavaScript 的运行结果。因此,TypeScript 的类型检查只是编译时的类型检查,而不是运行时的类型检查。一旦代码编译为 JavaScript,运行时就不再检查类型了。
TypeScript 类型
知识回顾
JS 的类型【8:7+1】
基本类型:number
、string
、boolean
、null
、undefined
、symbol
、bigInt
对象类型:Object
TS 类型:
基本类型:Number
,String
,Boolean
,Null
,undefined
,Symbol
,BigInt
对象类型:Function
,Object
,Array
,Enum
(枚举)
特殊类型:Void
,Any
,Unkown
,Never
联合类型:A | B
;
交叉类型:A & B
;
值类型
type
:取别名
Typeof:会将值/函数转化为类型
Any 类型
any
类型除了关闭类型检查,还有一个很大的问题,就是它会"污染"其他变量。它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错。
let x: any = "hello";
let y: number;
y = x; // 不报错;因为 x 是 any 类型,ts 会关闭 x 的类型检测
y * 123; // 不报错
y.toFixed(); // 不报错
unknow 类型
那么 unknow
解决了这个问题:变量 v
是 unknown
类型,赋值给 any
和 unknown
以外类型的变量都会报错,这就避免了污染问题。
在集合论上,unknown
也可以视为所有其他类型(除了 any
)的全集,所以它和 any
一样,也属于 TypeScript 的顶层类型。
Array 类型
TypeScript 数组有一个根本特征:所有成员的类型必须相同,但是成员数量是不确定的,可以是无限数量的成员,也可以是零成员。
由于数组的成员数量可以动态变化,所以 TypeScript 不会对数组边界进行检查,越界访问数组并不会报错。
只读数组
只读数组的含义就是:只允许进行查看,不允许进行任何的操作
const arr: readonly number[] = [1, 2, 3]
只读数组的写法:
- 关键字写法:
readonly number[]
- 泛型写法:
ReadonlyArray<number>
/ReadOnly<number[]>
- Const 断言:const 是一个常量,那么他设置的数组,就是只读数组
Const 断言
const 是一个常量,他设置的值,都是不能再变化的。
JS 的基本类型
- Symbol 类型会被断言为
unique symbol
类型 - 除了 Symbol 以外,const 断言后的类型,就会变为值类型
对于,对象类型而言,就会变为,只读类型
元组
特点:长度固定,并且每个索引位置的类型是确定的。
const s: [string, string, boolean] = ["a", "b", true];
场景:在 React 里,useState
、useReducer
、useContext
等 Hook 返回的就是元组。
当然元组的长度也可以不固定:
- 加上可选符号(?):可选运算符必须放在最后面
- 加上扩展运算符(...):扩展运算符用在元组的任意位置都可以,但是他后面的必须要数组/元组
let a: [number, number?] = [1]; // 可选运算符
let b: [string, ...number[]] = ["hello", 1, 2, 3] // 扩展运算符
函数类型
- 普通函数写法
- 箭头函数写法
// 写法一
const hello = function (txt: string) {
console.log("hello " + txt);
};
// 写法二
const hello: (txt: string) => void = function (txt) {
console.log("hello " + txt);
};
如果一个变量要套用另一个函数类型,有一个小技巧,就是使用 typeof
运算符
function add(x: number, y: number) {
return x + y;
}
const myAdd: typeof add = function (x, y) {
return x + y;
};
参数默认值
设置了默认值的参数,那么该参数是可选的。如果不传入该参数,它就会等于默认值。设有默认值的参数,如果传入 undefined
,也会触发默认值。
function createPoint(x: number = 0, y: number = 0): [number, number] {
return [x, y];
}
createPoint(); // [0, 0]
function f(x = 456) {
return x;
}
f2(undefined); // 456:设置了默认值的参数,即使传递的参数是 undefined,那么也是使用默认值
函数重载:
一个函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为。这种根据参数类型不同,执行不同逻辑的行为,称为函数重载(function overload)。
function reverse(str: string): string;
function reverse(arr: any[]): any[];
function reverse(stringOrArray: string | any[]): string | any[] {
if (typeof stringOrArray === "string")
return stringOrArray.split("").reverse().join("");
else return stringOrArray.slice().reverse();
}
函数重载的每个类型声明之间,以及类型声明与函数实现的类型之间,不能有冲突。
// 报错
function fn(x: boolean): void;
function fn(x: string): void; // 这一个和下面那个冲突了,他并不知道应该执行哪一个
function fn(x: number | string) {
console.log(x);
}
那么其实我们,函数重载也并不是必要的:**联合类型替代函数重载。**因为我们可以通过定义参数的类型【联合类型】,然后对参数类型进行判断,从而来执行不同的逻辑。
// 写法一
function len(s: string): number;
function len(arr: any[]): number;
function len(x: any): number {
return x.length;
}
// 写法二
function len(x: any[] | string): number {
return x.length;
}
对象类型【object】
对象类型比较宽泛,一般情况下,我们不要将类型直接定义为 Object 类型
属性名的索引类型【少用】
产生场景:有些时候,无法事前知道对象会有多少属性,比如外部 API 返回的对象,没法去定义这个对象的类型
索引类型里面,最常见的就是属性名的字符串索引。
type MyObj = {
[property: string]: string;
};
const obj: MyObj = {
foo: "a",
bar: "b",
baz: "c",
};
但是索引类型要少用,因为这样他会增大我们的类型范围!
接口【interface】
interface 可以表示对象的各种语法,它的成员有 5 种形式。
- 对象属性
interface A {
name: string
}
- 对象的属性索引
interface A {
[prop: string]: string;
}
- 对象方法
// 写法一
interface A {
f(x: boolean): string;
}
// 写法二[常写!!!]
interface B {
f: (x: boolean) => string;
}
// 写法三
interface C {
f: { (x: boolean): string };
}
- 函数
interface Add {
(a: number, b: number): number
}
- 构造函数
特性
继承【extends】
- 继承可以继承一个也可以继承多个;其实我们可以将它理解为,联合类型。
- 如果子接口与父接口存在同名属性,那么子接口的属性会覆盖父接口的属性。注意,子接口与父接口的同名属性必须是类型兼容的,不能有冲突,否则会报错。
- 接口可以继承接口,接口也可以继承类型,还有继承 Class
interface Style {
color: string;
}
interface Shape {
name: string;
}
interface Circle extends Style, Shape {
color: number; // 报错:类型不兼容
radius: number;
}
接口合并
多个同名接口会合并成一个接口。但是他们的同名属性必须可以兼容!
Type 和 interface 的区别
相同点:都可以用来描述对象
不同点:
Type 能够表示非对象;但是 interface 只能表示对象
tstype A = number; // type 有起别名的作用
interface
可以继承其他类型,type
不支持继承。interface 中,继承使用的是
extends
;但是 type 可以通过&
来实现增加属性的效果tstype Animal = { name: string; }; type Bear = Animal & { honey: boolean; }; interace Animal = { name: string; }; interface Bear extends Animal { honey: boolean; }
类型合并问题
Interface
的类型是可以合并的,但是同名type
就会报错。TypeScript
不允许使用type
多次定义同一个类型。interface 不可以使用属性映射【keyof】,但是 type 可以
tsinterface Point { x: number; y: number; } // 正确 type PointCopy1 = { [Key in keyof Point]: Point[Key]; }; // 报错 interface PointCopy2 { [Key in keyof Point]: Point[Key]; };
interface
无法表达某些复杂类型(比如交叉类型和联合类型),但是type
可以结论:一般情况下,优先考虑
interface
;如果会涉及到比较复杂的类型运算,那么才会使用type
。
Enum 枚举
Enum 结构的特别之处在于,它既是一种类型,也是一个值。绝大多数 TypeScript 语法都是类型语法,编译后会全部去除,但是 Enum 结构是一个值,编译后会变成 JavaScript 对象,留在代码中。
// 编译前
enum Color {
Red, // 0
Green, // 1
Blue, // 2
}
// 编译后
let Color = {
Red: 0,
Green: 1,
Blue: 2,
};
多个同名的 Enum 结构会自动合并。但是在合并的时候,只允许其中一个的首成员省略初始值,否则报错,并且合并的时候,不允许出现同名成员。
enum Foo {
A,
}
enum Foo {
B, // 报错
}