TS继承了JS的类型,并在这个基础上,定义了一套自己的类型系统。
基本类型
- boolean
- string
- number
- bigint
- symbol
- object
- undefined
- null
JS将值分成上面8种类型,TS 继承了 JS 的类型设计,所以这8种类型可以看作TS的基本类型,它们可以组合成复杂的TS类型。
需要注意的是,上面所有类型的名称都是小写字母。而首字母大写的Number、String、Boolean等在JS中都是内置对象,而不是类型名称。
另外,undefined和null比较特殊,既可以作为值,也可以作为类型。
// 第一个 undefined、null 表示类型
const a: undefined = undefined
const b: null = null
// 在 JS 中 object 类型包含对象、数组、函数
const x: object = { foo: 123 }
const y: object = [1, 2, 3]
const z: object = (n:number) => n + 1
包装对象类型
包装对象
- boolean
- string
- number
- bigint
- symbol
在JS中上面的5种类型属于原始类型(primitive value),都有对应的包装对象(wrapper object)。所谓“包装对象”,就是这些原始类型的值在需要时,自动产生的对象。
undefined与null也属于原始类型,但比较特殊,没有对应的包装对象。
const str = 'hello'
str.indexOf('e') // 1
上面的示例中,str执行了indexOf()方法,但在JS中只有对象才有方法。上面的代码之所以能够正常运行,就是因为原始值在读写变量或调用方法时,会自动创建一个对应的包装对象,通过这个对象进行相应的操作。
const str = 'hello'
// str.indexOf('e') // 1
// 1、创建对应的包装对象
let strObj = new String(str)
// 2、进行相应的操作(读取、设置变量或调用方法)
strObj.indexOf('e') // 1
// 3、销毁对象
strObj = null
当执行到str.indexOf('e')这行代码时,实际上是自动执行的上面三步操作。自动创建的包装对象,只存在于这行代码执行期间,执行完成立即销毁。
const s = 'hi'
s.name = 'foo'
console.log(s.name) // undefined
向s添加name属性时,会自动创建包装对象,将name属性添加到这个对象上,然后销毁。紧接着去打印s.name时,又会自动创建一个新的包装对象,然后去读取它的name属性。这与在给s添加name属性时创建的包装对象是完全独立的两个实例,那么从它身上读取的name属性时,自然是undefined。
symbol类型和bigint类型无法直接获取它们的包装对象,Symbol()和BigInt()不能作为构造函数使用
包装对象类型与字面量类型
由于包装对象的存在,导致每一个原始类型的值都有包装对象和字面量两种情况。
- Boolean 和 boolean
- String 和 string
- Number 和 number
- BigInt 和 bigint
- Symbol 和 symbol
const s1: String = 'hello'; // 正确
const s2: String = new String('hello'); // 正确
const s3: string = 'hello'; // 正确
const s4: string = new String('hello'); // 报错
上面示例中,String类型可以赋值为字符串的字面量,也可以赋值为包装对象。但是,string类型只能赋值为字面量,赋值为包装对象就会报错。绝大部分使用原始类型的场合,都应该使用字面量,不使用包装对象。
Object 类型与 object 类型
TS的对象类型也有大写Object和小写object两种。
Object 类型
大写的Object类型代表JS中的广义对象。所有可以转成对象的值,都是Object类型。事实上,除了undefined和null这两个值不能转为对象,其他任何值都可以赋值给Object类型(原始类型的值可转成包装对象)。
let obj: Object;
// 空对象 {} 是 Object 类型的简写形式
// let obj: {};
obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a: number) => a + 1;
obj = undefined; // 报错
obj = null; // 报错
object 类型
小写的object类型代表JS中的狭义对象,只包含引用类型。
let obj: object;
obj = { foo: 123 };
obj = [1, 2];
obj = (a: number) => a + 1;
obj = true; // 报错
obj = 'hi'; // 报错
obj = 1; // 报错
在使用对象类型时,只希望包含真正的对象,因此建议总是使用小写类型的object。
const o1: Object = { foo: 0 };
const o2: object = { foo: 0 };
o1.toString() // 正确
o1.foo // 报错
o2.toString() // 正确
o2.foo // 报错
注意,无论是大写的Object类型,还是小写的object类型,都只包含JS内置对象原生的属性和方法,用户自定义的属性和方法都不存在于这两个类型之中。
值类型
TS规定,单个值也可以作为一种类型,称为”值类型“。
let n: 123 // n 的类型为 123
n = 123
n = 456 // 报错
上面的示例中,n的类型为123,导致它只能赋值为123,被赋予其他值时都会报错。
// x 的类型是 "https"
const x = 'https';
// y 的类型是 string
const y: string = 'https';
上面的示例中,x是通过const声明的常量,没有设置类型,TS将他的类型推断为值类型https。与y不同,y的类型已被设置,不需要推断。
// x 的类型是 { foo: number }
const x = { foo: 1 };
使用const声明对象时,不会推断成值类型。这是因为即使是通过const声明的对象,内部的属性值依然是可以改变的。
子类型、父类型
TS中定义了一个术语,如果A类型的值可以赋值给B类型,那么A类型可称为B类型的子类型(子集),B类型称为A类型的父类型(父集或超集)。凡是可以使用父类型的地方,都可以使用子类型,但是反过来不行。一个类型的子类型和父类型都可能不存在,也可能存在多个。
const num_1 = 1
let num_2 = 2
let num_3 = new Number(3)
num_2 = num_1 // 类型为:1
num_3 = num_1 // 类型为:number
num_3 = num_2 // 类型为:Number
上面的示例中num_1类型为1,num_2的类型为number,num_3的类型为Number。num_1可以赋值给num_2、num_3,那么num_1的类型是num_2、num_3类型的子类型。num_2也可以赋值给num_3,因此num_2也是num_3的子类型。
从这个角度看,那么any类型就是其它所有类型的父类型,unknown是除了any类型以外的其它类型的父类型,never是任何类型的子类型。any和unknown也被称为顶层类型,never被称为底层类型。
联合类型
联合类型指的是多个类型组成的一个新类型,使用符号|表示。
联合类型A|B表示,任何一个类型只要属于A或B,就属于联合类型A|B。其实就是A类型和B类型,都是A|B类型的子类型。
let untrue: '' | 0 | false | undefined | null = false
untrue = ''
untrue = 0
untrue = false
上面的示例都是由值类型(注意是类型,不是值)组成的联合类型,非常清晰地表达了变量的取值范围。
交叉类型
交叉类型指的多个类型组成的一个新类型,使用符号&表示。
交叉类型A&B表示,任何一个类型必须同时属于A和B,才属于交叉类型A&B,即A&B类型的值必须同时满足A和B类型的特征。
let x: number & string
上面的示例中,x的值必须满足即是数字又是字符串,当然不存在这样的值(或者说是仅存在空值),因此TS认为x的类型实际是never。
交叉类型的主要用途是表示对象的合成。
let numObj: { toFixed: object } & object
numObj = new Number(123)
上面示例中,numObj的类型是{ toFixed: object } & object,因此numObj需要被赋值为具有toFixed属性的对象,并且toFixed的属性值也要是一个对象。那么Number构造函数的实例,即是对象又自带toFixed()方法,满足这个交叉类型的特征。
let num: (1 | 2 | 3) & (3 | 4 |5)
num = 3
联合类型1 | 2 | 3、3 | 4 |5交叉,组成一个新的交叉类型(1 | 2 | 3) & (3 | 4 |5)。
type
type命令用来定义一个类型的别名。
type Untrue = '' | 0 | false | undefined | null
let n: Untrue = 0
let s: Untrue = ''
let b: Untrue = false
定义别名可以让复杂的类型更方便使用和修改。
type World = "world";
type Greeting = `hello ${World}`;
别名支持使用表达式,也可以在定义一个别名时,使用另一个别名,即别名允许嵌套。
typeof
JS语法中typeof运算符返回操作数(一个值)的类型,是一个字符串。
TS语法中typeof运算符也返回操作数(一个值)的类型,但不再是字符串,而是一个TS类型。更需要注意的是,typeof也只能在TS的类型运算(即跟类型相关的代码之中)中使用,并且它的参数只能是标识符,相关逻辑在编译时也会被删除。
const n = 5
// JS 语法
typeof n
// 这里的 typeof 是 TS 语法,用于获取 n 的 TS 类型
let n_5: typeof n = n // TS 类型为 5
const obj = { foo: 'hello', bar: 123 }
// 获取 objType 类型,组成交叉类型,并定义别名
type newType = typeof obj & { biz: true } // { foo: string, bar: number, biz: true }
const newObj: newType = {
foo: 'world',
bar: 999,
biz: true
}
typeof 是一个很重要的TS运算符,有些场合不知道某个变量objType的类型,这时使用typeof objType就可以获得它的类型。
参考文献
《JavaScript 高级程序设计(第4版)》