TypeScript入坑
- 类型注解和类型推断
- any、unknow、never类型
- any类型
- unknow类型
- never类型
- 类型系统
- 基本类型
- 包装对象类型
- Object类型与object类型
- undefined和null的特殊性
- 值类型
- 联合类型
- 交叉类型
- type命令
- typeof运算符
- 类型的兼容
- 数组
- 简介
- 数组的类型推断
- 只读数组
- 元组 tuple
- 简介
- 函数
- 简介
- Function类型
- 可选参数
- 参数结构
- rest参数
- 函数重载
- Symbol类型
- 简介
- unique symbol
- 对象
- 简介
- 结构类型原则
- 严格字面量检查
- 最小可选属性规则
类型注解和类型推断
类型注解(type annotation) :告诉 TS 变量是什么类型。例如:
// 当 TS 无法推断出变量类型的时候需要添加类型注解
function getTotal(firstNumber: number, secondNumber: number) {
return firstNumber + secondNumber
}
const total = getTotal(1, 2)
// 其他的情况
interface Person {
name: string
}
const rawData = '{"name": "Simon"}'
const newData: Person = JSON.parse(rawData)
// 一个变量是一个数字类型,后续要变成字符串。类似或运算符
let temp: number | string = 123
temp = '456'
类型推断(type inference):TS 会自动的去尝试分析变量的类型。例如:
// 这就是典型的类型推断,它们的类型是 number 而且值永远都不会变的
const firstnumber = 1
const secondNumber = 2
const total = firstNumber + secondNumber
any、unknow、never类型
any类型
any
类型表示没有任何限制,该类型的变量可以赋予任意类型的值。
let x: any;
x = 1; // 正确
x = "foo"; // 正确
x = true; // 正确
any
类型主要适用于以下两个场合:
- 出于特殊原因,需要关闭某些变量的类型检查
- 为了适配以前老的JavaScript项目,让代码快速迁移到TypeScript,可以把变量类型设为
any
。
any
存在类型推断问题,如果TypeScript无法推断出类型,就会认为该变量的类型是 any
。
function add(x, y) {
return x + y;
}
add(1, [1, 2, 3]); // 不报错
any
存在污染问题,any
类型除了关闭类型检查,还有一个很大的问题是会”污染“其他变量。它可以赋值给其他任何类型的变量,导致其他变量出错。
let x: any = "hello";
let y: number;
y = x; // 不报错
y * 123; // 不报错
y.toFixed(); // 不报错
unknow类型
为了解决 any
类型”污染“其他变量的问题,TypeScript 3.0引入了unknown
类型。它与any
含义相同,但使用有一些限制,可以理解为严格版的any
。
unknown
类型赋值给 any
和 unknown
以外类型的变量都会报错,不能调用 unknown
类型变量的方法和属性,且只能进行比较运算、typeof
运算符和 instanceof
运算符。
let v: unknown = 123;
let v1: boolean = v; // 报错
let v2: number = v; // 报错
let v1: unknown = { foo: 123 };
v1.foo; // 报错
let v2: unknown = "hello";
v2.trim(); // 报错
let a: unknown = 1;
a + 1; // 报错
a === 1; // 正确
🤔 那么怎么才能使用
unknown
类型变量呢?
只有经过”类型缩小“,unknown
类型变量才可以使用。
let a: unknown = 1;
if (typeof a === "number") {
let r = a + 10; // 正确
}
never类型
为了保持与集合论的对应关系,以及类型运算的完整性,TypeScript 还引入了“空类型”的概念,即该类型为空,不包含任何值。
由于不存在任何属于“空类型”的值,所以该类型被称为never
,即不可能有这样的值。
类型为 never
的变量不可能给它赋任何值,否则会报错。
而 never
类型的变量可以赋值给任意其他类型。
function f(): never {
throw new Error("Error");
}
let v1: number = f(); // 不报错
let v2: string = f(); // 不报错
上面函数 f()
会抛出错误,所以返回值可以写成 never
,即不可能返回任何值。
🤔 为什么
never
类型可以赋值给任意其他类型呢?
这也跟集合论有关,空集是任何集合的子集。TypeScript 就相应规定,任何类型都包含了never
类型。因此,never
类型是任何其他类型所共有的,TypeScript 把这种情况称为“底层类型”(bottom type)。
总之,TypeScript 有两个“顶层类型”(any
和unknown
),但是 “底层类型”只有never
唯一一个。
类型系统
基本类型
基础类型: boolean
, number
, string
, void
, undefined
, symbol
, null
包装对象类型
JavaScript中为了原始值在需要时可以执行一些方法,有”包装对象“的概念。
"hello".charAt(1); // 'e'
上面代码,字符串"hello"执行了
charAt()
方法,实际上是在运行时自动将原始值包装成了对象。
为了区分字面量和包装对象,TypeScript对5种原始类型分别提供了大写和小写两种类型:
- Boolean 和 boolean
- String 和 string
- Number 和 number
- BigInt 和 bigint
- Symbol 和 symbol
建议只使用小写类型,不使用大写类型。 因为绝大部分使用原始类型的场合,都是使用字面量,不使用包装对象。而且,TypeScript 把很多内置方法的参数,定义成小写类型,使用大写类型会报错,如Math.abs()。
Symbol()和BigInt()这两个函数不能当作构造函数使用,所以没有办法直接获得 symbol 类型和 bigint 类型的包装对象,因此Symbol和BigInt这两个类型虽然存在,但是完全没有使用的理由。
Object类型与object类型
Object
类型代表了JavaScript语言里面广义对象,所有可以转成对象的值,都是 Object
类型。 事实上,除了 undefined
和 null
这两个值不能转为对象,其他任何值都可以赋值给 Object
类型。
object
类型代表JavaScript语言里面狭义对象,只包含对象、数组和函数。
undefined和null的特殊性
undefined
和 null
即是值,又是类型。
作为值,它们的特殊之处在于,任何其他类型的变量都可以赋值为 undefined
或 null
。
这样其实不利于发挥类型系统的优势,我们可以打开编译选项 strictNullChecks
, 这样 undefined
和null
不能赋值给其他类型的变量(除了any
和unknown
)。
值类型
TypeScript规定,单个值也是一种类型,称为”值类型“。
let x: "hello";
x = "hello"; // 正确
x = "world"; // 报错
联合类型
联合类型指的是多个类型组成的一个新类型,使用符号 |
表示。
let x: string | number;
x = 123; // 正确
x = "abc"; // 正确
联合类型可以与值类型相结合,表示一个变量的值有若干种可能。
let gender: "male" | "female";
let rainbowColor: "赤" | "橙" | "黄" | "绿" | "青" | "蓝" | "紫";
交叉类型
交叉类型指的是多个类型组成的一个新类型,使用符号 &
表示。
let x: number & string;
上面实例中,变量x不可能同时是数值和字符串,所以认为x类型为
never
。
交叉类型的主要用途是表示对象的合成。
let obj: { foo: string } & { bar: string };
obj = {
foo: "hello",
bar: "world",
};
type命令
type
命令用来定义一个类型的别名。
type Age = number;
let age: Age = 55;
别名不允许重名,作用域为块级作用域。
type Color = "red";
if (Math.random() < 0.5) {
type Color = "blue";
}
typeof运算符
在JavaScript种,typeof
运算符只可能返回八种结果(对应8种基本类型),而且都是字符串。
TypeScript将 typeof
运算符移植到了类型运算,它的操作数依然是一个值,但返回的不是字符串,而是该值的TypeScript类型。
const a = { x: 0 };
type T0 = typeof a; // { x: number }
type T1 = typeof a.x; // number
也就是说,同一段代码可能存在两种typeof运算符。
let a = 1;
let b: typeof a; // 类型运算
if (typeof a === "number") { // 值运算
b = a;
}
👇 上述代码编译结果如下:
let a = 1;
let b;
if (typeof a === "number") {
b = a;
}
类型的兼容
TypeScript 的类型存在兼容关系,某些类型可以兼容其他类型。
type T = number | string;
let a: number = 1;
let b: T = a;
很容易理解,上述变量a的类型是变量b的子类型,所以a可以赋值给b。
数组
简介
TypeScript 数组有一个根本特征:所有成员的类型必须相同,但是成员数量是不确定的,可以是无限数量的成员,也可以是零成员。
let arr1: number[] = [1, 2, 3];
let arr2: Array<number> = [1, 2, 3];
数组的类型推断
如果没有声明类型,初始化和赋值都会自动推断类型。
const arr = [];
arr; // 推断为 any[]
arr.push(123);
arr; // 推断类型为 number[]
arr.push("abc");
arr; // 推断类型为 (string|number)[]
只读数组
readonly
关键字声明只读数组。
const arr: readonly number[] = [0, 1];
const a1: ReadonlyArray<number> = [0, 1];
const a2: Readonly<number[]> = [0, 1];
也可以 as const
告诉TypeScript,推断类型时将数组推断为只读数组。
const arr = [0, 1] as const;
arr[0] = [2]; // 报错
元组 tuple
简介
元组(tuple)是 TypeScript 特有的数据类型,JavaScript 没有单独区分这种类型。它表示成员类型可以自由设置的数组,即数组的各个成员的类型可以不同。
元组必须明确声明每个成员的类型。
const s: [string, string, boolean] = ["a", "b", true];
函数
简介
函数的类型声明,需要在声明函数时,给出参数的类型和返回值的类型。
function hello(txt: string): void {
console.log("hello " + txt);
}
const repeat = (str: string, times: number): string => str.repeat(times);
Function类型
TypeScript 提供 Function 类型表示函数,任何函数都属于这个类型。
function doSomething(f: Function) {
return f(1, 2, 3);
}
Function 类型的函数可以接受任意数量的参数,每个参数的类型都是
any
,返回值的类型也是any
,代表没有任何约束,所以不建议使用这个类型,给出函数详细的类型声明会更好。
可选参数
函数体内部用到可选参数时,需要判断该参数是否为 undefined
。
let myFunc: (a: number, b?: number) => number;
myFunc = function (x, y) {
if (y === undefined) {
return x;
}
return x + y;
};
另外注意可选参数只能定义在参数列表末尾。
参数结构
函数参数如果存在变量解构,类型写法如下。
function f([x, y]: [number, number]) {
// ...
}
function sum({ a, b, c }: { a: number; b: number; c: number }) {
console.log(a + b + c);
}
可以配合 type
命令一起使用,使代码更简洁。
type ABC = { a: number; b: number; c: number };
function sum({ a, b, c }: ABC) {
console.log(a + b + c);
}
rest参数
rest
参数表示函数剩余的所有参数,它可以是数组(剩余参数类型相同),也可能是元组(剩余参数类型不同)。
// rest 参数为数组
function joinNumbers(...nums: number[]) {
// ...
}
// rest 参数为元组
function f(...args: [boolean, number]) {
// ...
}
函数重载
有些函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为。这种根据参数类型不同,执行不同逻辑的行为,称为函数重载(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();
}
Symbol类型
简介
Symbol 值通过 Symbol()
函数生成。在 TypeScript 里面,Symbol 的类型使用symbol
表示。
let x: symbol = Symbol();
let y: symbol = Symbol();
x === y; // false
unique symbol
symbol
类型包含所有的Symbol值,但是无法表示某一个具体的Symbol值。
为了解决这个问题,TypeScript设计了 symbol
的一个子类型 unique symbol
,它表示单个的、某个具体的Symbol值。
因为其表示单个值,所以是不能修改的,只能用 const
命令声明,不能用 let
声明。
// 正确
const x: unique symbol = Symbol();
// 等同于
const x = Symbol();
// 报错
let y: unique symbol = Symbol();
简单理解,这样声明的不同变量虽然都是 unique symbol
,但其实是两个值类型。
const a: unique symbol = Symbol();
const b: unique symbol = Symbol();
a === b; // 报错
const a: unique symbol = Symbol();
const b: typeof a = a; // 正确
unique symbol
类型的一个作用,就是用作属性名,这可以保证不会跟其他属性名冲突。如果要把某一个特定的 Symbol 值当作属性名,那么它的类型只能是 unique symbol
,不能是 symbol
。
const x: unique symbol = Symbol();
const y: symbol = Symbol();
interface Foo {
[x]: string; // 正确
[y]: string; // 报错
}
对象
简介
// 属性类型以分号结尾
type MyObj = {
x: number;
y: number;
};
// 属性类型以逗号结尾
type MyObj = {
x: number;
y: number;
};
属性的类型可以用分号结尾,也可以用逗号结尾。
结构类型原则
只要对象 B 满足 对象 A 的结构特征,TypeScript 就认为对象 B 兼容对象 A 的类型,这称为“结构类型”原则(structual typing)。
const B = {
x: 1,
y: 1,
};
const A: { x: number } = B; // 正确
严格字面量检查
如果对象使用字面量表示,会触发 TypeScript 的严格字面量检查(strict object literal checking)。如果字面量的结构跟类型定义的不一样(比如多出了未定义的属性),就会报错。
const point: {
x: number;
y: number;
} = {
x: 1,
y: 1,
z: 1, // 报错
};
如果等号右边不是字面量,而是一个变量,根据结构类型原则,不会报错。
const myPoint = {
x: 1,
y: 1,
z: 1,
};
const point: {
x: number;
y: number;
} = myPoint; // 正确
最小可选属性规则
如果一个对象的所有属性都是可选的,会触发最小可选属性规则。
type Options = {
a?: number;
b?: number;
c?: number;
};
const obj: Options = {
d: 123, // 报错
};
上面代码,类型Options是一个对象,它的所有属性都是可选的,这导致任何对象实际都符合Options类型。所以TypeScript规定不能所有的可选属性都不存在。