类型术语
看下面这个函数:
function squareOf(n: number) {
return n * n;
}
squareOf(2)
squareOf('z') // ts(2345): 类型“string”的参数不能赋给类型“number”的参数
显然,这个函数只能操作数字,如果传入数字以外的值,TypeScript将立即报错。这只是一个简单的例子,不过足以引入TypeScript类型的一些关键概念。从上述代码可以看出:
- squareOf的参数n被约束为number。
- 这个值可以赋值给number(与number兼容)
如果没有类型注解,squareOf的参数不受任何约束,可以传入任何类型的值。一旦加上约束,TypeScript将检查每次对该函数的调用,确保传入兼容的参数。
另外,也可以把类型注解理解为某种界限,在上面例子中,我们告诉TypeScript,n的上限是number,因此传给squareOf的值有且只有一个数字。如果不传入或者超过数字的值(如数组、对象或者字符串等),那就会报错。
类型浅谈
any
any是类型的教父。为达目的,它不惜一切代价,所以不要轻易的使用它,否则 TypeScript将变得没有任何意义。
为什么呢?因为any包含所有值,而且可以对其做任何操作!any类型的值就像常规的JavaScript一样,类型检查器完全发挥不了作用。所以远离any,除非万不得已。
在极少数情况下,可以像下面这样使用:
let a:any = 1;
let b:any = ['danger'];
let c:any = a + b;
注意,正常情况下,第三个语句将报错,毕竟谁会计算一个数字和一个数组的和呢?但是我们告诉TypeScript他们的类型为any,就不会报错了。
如果想使用any,一定要显示注解! 因为如果TypeScript推导出值的类型为any,将抛出运行时异常,在编辑器显示一条红色的波浪线。如果显示的标注为any类型,则不会报错,因为这样就是告诉TypeScript你知道自己在做什么。
unknown
与any类似,unknown也表示任何值,但是TypeScript会要求你再做检查,细化类型。所以,如果你确实无法预知一个值的类型,应该使用unknown而不是使用any。
unknown支持以下几种操作:
- 比较(使用||、&&、?)
- 否定(使用!)
- 类型判断(使用typeof、instanceof)
unknown的用法如下:
let a:unknown = 10;
let b = a === 123;
let c = !a;
let d = a + 10; // 对象的类型为 "unknown"。ts(2571)
if(typeof a === 'number') {
let e = a + 10;
}
通过示例,我们可以了解到unknown的用法:
- TypeScript不会把任何值推导为unknown类型,必须显示注解。
- unknown类型的值可以进行比较,否定,类型判断的操作
- 执行操作时,不能假定unknown类型的值为某种特定类型,必须先证明它确实是某种特定类型
boolean
boolean类型有且只有两个值:true、false。该类型的值可以比较(使用==、===、||、&&和?),可以否定(使用!),此外则没有什么操作了,boolean的用法如下:
let a = true; // boolean
var b = false; // boolean
const c = true; // true
let d: boolean = true; // boolean
let e: true = true; // true
let f: true = false; // 不能将类型“false”分配给类型“true”。ts(2322)
这个示例表明我们可以通过多种方式告诉TypeScript一个值的类型为boolean:
- 可以让TypeScript推导出值的类型为boolean(a和b)
- 可以让TypeScript推导出值为某个具体的布尔值(c)
- 可以明确的告诉TypeScript,值的类型为boolean(d)
- 可以明确的告诉TypeScript,值的类型为某个具体的布尔值(e和f)
一般来说,我们在程序中采用第一种和第二种方式,极少情况下使用第四种方式。几乎从不使用第三种方式。
number
number包含所有数字:整数、浮点数、正数、负数、Infinity、NaN等。显然,数字可以做算术运算,以下是示例:
let a = 1234; // number
var b = a * Infinity; // number
const c = 5678; // 5678
let d = a < c; // boolean
let e: number = 100; // number
let f: 26.17 = 26.17; // 26.17
let g: 26.17 = 30; // 不能将类型“30”分配给类型“26.17”。ts(2322)
与boolean类型类似,把值声明为number类型也有四种方式:
- 可以让TypeScript推导出值的类型为number(a和b)
- 可以让TypeScript推导出值为某个具体的数字(c)
- 可以明确的告诉TypeScript,值的类型为number(e)
- 可以明确的告诉TypeScript,值的类型为某个具体的数字(f和g)
同样的,我们通常让TypeScript自己推导类型(第一种和第二种方式),我们偶尔也会使用第四种方式去固定一个明确的数字。如果没有特殊需求,我们不使用第三种方式。
bigint
bigint是JavaScript和TypeScript新引入的类型,在处理较大整数时,不用再担心舍入误差。bigint类型支持运算相关操作。用法如下:
let a = 12345n; // bigint
const b = 5678n; // 5678n
var c = a + b; // bigint
let d = a < c; // boolean
let e = 88.8n; // bigint 文本必须是整数。ts(1353)
let f: bigint = 123n; // bigint
let g: 100n = 100n; // 100n
let h: 100n = 100; // 不能将类型“100”分配给类型“100n”。ts(2322)
与boolean和number一样,声明为bigint类型也有四种方式。尽量让TypeScript自行推导。
string
string包含所有字符串,以及可以对字符串执行的操作。示例如下:
let a = 'hello'; // string
const b = 'world'; // world
let c = a + ' ' + b; // string
let d: string = 'hello'; // string
let e: 'hello' = 'hello'; // hello
let f: 'hello' = 'tst'; // 不能将类型“"tst"”分配给类型“"hello"”。ts(2322)
与boolean和number一样,声明为string也是四种方式。尽量让TypeScript自行推导。
symbol
symbol是一个相对较新的语言特性,symbol代表着符号(或者唯一字串),经常用于代替对象和映射的字符串键,确保使用正确的已知键,示例如下:
let a = Symbol('a'); // symbol
let b: symbol = Symbol('b'); // symbol
let c = a === b; // boolean
let d = a + 'x' // “+”运算符不能应用于类型 "symbol"。ts(2469)
在JavaScript中Symbol('a')使用指定的名称新建一个符号,这个符号就是唯一的,不与其他符号相等,即便再使用相同的名称创建一个符号也是如此。符号经过推导得到的类型是symbol,此外也可以显示声明为unique symbol类型:
const a = Symbol('a'); // typeof a
const b: unique symbol = Symbol('b'); // typeof b
let c: unique symbol = Symbol(('c')); // 类型为 "unique symbol" 的变量必须为 "const"。ts(1332)
let d = b === b;
let e = a === b; // 此条件将始终返回 "false",因为类型 "typeof a" 和 "typeof b" 没有重叠。ts(2367)
通过这个示例得知:
- 使用const声明的符号,会被推导为 unique symbol类型,在代码编辑器中显示为 typeof yourVariableName,而不是unique symbol。
- 可以显示注解const变量的类型为unique symbol。
- unique symbol类型的变量必须为const。
- TypeScript在编译时知道一个unique symbol类型的值不会与另一个unique symbol类型的值相等
对象
在TypeScript中,使用类型描述对象有好几种方式。
把一个值声明为object类型
let a: object = {
name: 'zhangsan'
}
console.log(a.name) // 类型“object”上不存在属性“name”。ts(2339)
可以看到,把一个值显示的声明为object类型,做不了任何操作。因为object对值知之甚少,只能表示该值是一个JavaScript对象。
让TypeScript自动推导
let a = {
name: 'zhangsan'
} // let a: {
// name: string;
// }
console.log(a.name)
这种方式,TypeScript会自动推导出来对象的类型,当然我们也可以自己在花括号({})内明确描述。
对象字面量句法
let a: {
name: string
} = {
name: 'zhangsan'
} // let a: {
// name: string;
// }
console.log(a.name)
对象字面量句法的意思是:“这个东西的结构是这样的。”,这个“东西”可能是一个对象字面量,也可能是一个类:
class Person {
constructor(public name: string, public age: number){} // public 是this.name = name 的简写模式
}
let a: {name: string, age: number} = new Person('zhangsan', 26);
我们看看如果添加额外的属性或者缺少必要的属性会发生什么:
let a: {name: string, age: number};
a = {
name: 'zhangsan' // 类型 "{ name: string; }" 中缺少属性 "age",但类型 "{ name: string; age: number; }" 中需要该属性。ts(2741)
}
a = {
name: 'zhangsan',
age: 25,
school: 'beida' // 不能将类型“{ name: string; age: number; school: string; }”分配给类型“{ name: string; age: number; }”。ts(2322)
}
默认情况下,TypeScript对对象的属性要求十分严格。那么,有没有什么办法告诉TypeScript某个属性是可选的,或者实际的属性可能比计划的多呢,当然有:
let a: {
name: string
age?: number
[key: number]: boolean
};
- a 有个类型为string的属性name
- a 可能有个类型为number的属性age,如果有属性age,其值可以为undefined
- a可能有任意多个数字属性,其值为布尔值
下面,我们看看可以把哪些值赋值给a:
a = {
name: 'zhangsan'
}
a = {
name: 'zhangsan',
age: undefined
}
a = {
name: 'zhangsan',
age: 13
}
a = {
name: 'zhangsan',
age: 13,
10: true
}
a = {
name: 'zhangsan',
age: 13,
10: true,
20: false
}
a = {} // 类型 "{}" 中缺少属性 "name",但类型 "{ [key: number]: boolean; name: string; age?: number | undefined; }" 中需要该属性。ts(2741)
a = {name: 'zhangsan', 10: 'lisi'} // 不能将类型“string”分配给类型“boolean”。ts(2322)
[key: T] U句法称为索引签名,通过这种方式告诉TypeScript,指定的对象可能有更多的键。这种句法的意思是,“在这个对象中,类型为T的键对应的值为U类型。”
索引签名还有一条规则:键的类型(T)必须可赋值给number或者string。
索引签名中的键的名称可以是任何词,不一定非要使用key
声明对象类型时,可选符号(?)不是唯一可用的修饰符。还可以使用readonly修饰符把字段标记为只读:
let a: {
readonly name: string
};
a = {
name: 'zhangsam'
}
a = {
name: 'lisi'
}
a.name = 'wangwu' // 无法分配到 "name" ,因为它是只读属性。ts(2540)
需要注意的是,我们依然可以重新为a这个对象整体赋值,只是不可以单独在重新设置readonly属性的值。
对象字面量句法还有一个特例:空对象类型({})。除了null和undefined之外的任何类型都可以赋值给空对象类型,使用起来比较复杂,应该尽量避免。
let a: {}
a = {
name: 'zhangsan'
};
a = 1;
a = true;
a = 'zhangsan';
a = Symbol('one');
最后,还有一种声明对象类型的方式:Object。这与{}的作用基本一样,应该尽量避免使用。
综上所述,在TypeScript中,声明对象类型有四种方式:
- object类型。如果需要一个对象,但对对象的字段没有要求,可以使用这种方式。
- 自动推导。让TypeScript自动推导出对象的类型。
- 对象字面量句法。如果我们知道对象有哪些字段,可以使用这种方式。
- Object类型。尽量避免。
数组
与在JavaScript中一样,TypeScript中的数组也是特殊类型的对象。示例如下:
let a = [1,2,3] // number[]
let b = ['a', 'b'] // string[]
let c: string[] = ['a']; // string[]
let d = [1, 'a']; // (string | number)[]
const e = [1]; // number[]
let f = ['a'];
f.push('b');
f.push(1); // 类型“number”的参数不能赋给类型“string”的参数。ts(2345)
let g = []; // any[]
g.push(1);
g.push('2')
TypeScript支持两种注解数组类型的句法:T[]和Array< T >。二者的作用和性能没有差别。
根据上面的示例,只有c是显示注解类型。而且,TypeScript对什么样的值可以放入数组也有规定。一般情况下,数组应该保持同质。
如果数组中的元素不同质,那么使用之前则需要检查。看下面示例:
let d = [1, 'a']; // (string | number)[]
d.map(item => {
if(typeof item === 'number') {
return item * 3
};
return item.toUpperCase()
})
为此,必须使用typeof检查每个元素的类型。
与对象一样,使用const声明数组并不会导致TypeScript推导出来的范围变得更窄,所以推导出的d的类型依然是(string | number)[]。
g比较特殊,初始化空数组时,TypeScript不知道数组中元素的类型,推导出的类型为any。向数组中添加元素时候,TypeScript开始拼凑数组的类型。当数组离开定义时所在的作用域后,TypeScript才最终确定一个类型,不再扩张,否则一直可以扩张。
function buildArray () {
let a = [];
a.push(1);
a.push('zhangsan');
return a;
}
let b = buildArray(); // (string | number)[]
b.push(true); // 类型“boolean”的参数不能赋给类型“string | number”的参数。ts(2345)
元组
元组是array的子类型,是定义数组的一种特殊方式,长度固定,各索引位置上的值具有固定的已知类型。与其他类型不同,声明元组必须显示注解类型。因为创建元组使用的句法和数组是一样的(都是使用方括号),而TypeScript遇到方括号会推导出数组的类型。
let a: [number] = [1];
let b: [number, string, boolean] = [1, 'zhangsan', true];
b = [2, 'lisi', 'wangwu']; // 不能将类型“string”分配给类型“boolean”。ts(2322)
元组也支持可选的元素。如下:
let a: [number, string?][] = [
[1],
[2, 'zhangsan'],
[3, 'wangwu']
];
等价于:
let a: ([number] | [number, string])[] = [
[1],
[2, 'zhangsan'],
[3, 'wangwu']
]
元组也支持剩余元素。如下:
let a: [string, number, ...string[]] = ['zhangsan', 1, 'lisi'];
元素能够正确定义元素类型不同的列表,还能确定列表的长度,所以使得元组比数组更加安全,应该经常使用。
只读数组与元组
TypeScript支持只读数组类型,用于创建不可变的数组。只读数组不能就地更改。若想创建只读数组,要显示注解类型。
let arr: readonly number[] = [1,2,3];
let age = arr[1]; // number
arr[1] = 5; // 类型“readonly number[]”中的索引签名仅允许读取。ts(2542)
arr.push(3); // 类型“readonly number[]”上不存在属性“push”。ts(2339)
我们知道,注解数组类型还可以使用Array。类似的,声明只读数组和元组,也可以使用长格式句法:
type a = readonly number[]; // readonly number[]
type b = ReadonlyArray<number>; // readonly number[]
type c = Readonly<number[]>; // readonly number[]
type d = readonly [number]; // readonly [number]
type e = Readonly<[number]>; // readonly [number]
注意,只读数组不可变的特性能让代码更易于理解,不过其背后提供支持的仍然是常规的JavaScript数组。这意味着,即使只对数组做小小的改动,为了只读特性,也需要复制整个原数组,所以对于大型数组来说,会造成很大的性能影响。
如果想大量使用不可变数组,可以使用一种更为高效的视线,Lee Byron开发的immutable包就很不错。
null、undefined、void和never
在TypeScript中,undefined类型只有undefined一个值,null类型也只有null一个值。
undefined表示尚未定义,null表示缺少值(例如在计算一个值的过程中遇到错误)。
除了null和undefined之外,TypeScript还有void和never类型。void是函数没有显示返回任何值时的返回类型,而never是函数根本不返回(例如函数抛出异常,或者永远运行下去)时使用的类型。
/ 返回 undefined
function a() {
return undefined;
};
// 返回null
function b() {
return null;
}
// 返回void
function c() {
console.log('void...');
}
// 返回never
function d() {
return new Error('error...');
}
// 返回never
function e() {
while(true) {
console.log('do something....');
}
}
如果说unknown是其他每个类型的父类型,那么never就是其他每个类型的子类型。我们可以把never理解为“兜底类型”。这意味着,never类型可以赋值给其他任何类型(当然这并没有任何操作的意义),下面是例子:
function a():never {
while(true) {
console.log('do something....');
}
}
var b: number = a();
枚举
枚举的作用是列举类型中包含的各个值。这是一种无序的数据结构,把键映射到值上。
枚举分为两种:字符串到字符串之间的映射和字符串到数字之间的映射。如下:
enum Language {
English,
Chinese,
Spanih
}
TypeScript可以自动为枚举中的各个成员推导出对应的数字,也可以自动设置。上面代码等价于:
enum Language {
English = 0,
Chinese = 1,
Spanih = 2
}
枚举中的值使用点号或者方括号访问,和对象一样:
const fristLanguage = Language.English; // 0
const secondLanguage = Language.Chinese; // 1
一个枚举可以分成多次声明,TypeScript会自动合并。注意,如果分开枚举,TypeScript只能推导出一部分的值,因此最好是给每个成员显示赋值。
enum Language {
English = 0,
Chinese = 1
}
enum Language {
Spanih
}
const fristLanguage = Language.Spanih; // 0
const secondLanguage = Language.Chinese; // 1
上面的例子充分说明了显示赋值的必要性,Language.Spanih由于没有显示赋值,被推导成了0。
如果不是分开枚举,TypeScript推导出来的值还是值得信赖的:
enum Language {
English = 0,
Chinese = 1,
Spanih
}
const fristLanguage = Language.Spanih; // 2
const secondLanguage = Language.Chinese; // 1
枚举的值也可以是字符串,甚至混用字符串和数字:
enum Color {
Red = '#c10000',
Blue = '#007ac1',
White = 255,
Black
}
const fristColor = Color.Red; // #c10000
const secondColor = Color.Black; // 256
TypeScript比较灵活,既允许通过值访问枚举,也可以通过键访问(注意是键,不是下标),不过很容易导致问题:
enum Color {
Red = '#c10000',
Blue = '#007ac1',
White = 255,
Black
}
const fristColor = Color[255]; // White
const secondColor = Color[4]; // undefined
const thirdColor = Color.green; // 类型“typeof Color”上不存在属性“green”。ts(2339)
上面代码可以看到,Color[4]并没有报错,但是Color.green会有报错提示。
为了避免这种不安全的访问,可以通过const enum指定使用枚举的安全子集:
const enum Color {
Red = '#c10000',
Blue = '#007ac1',
White = 255,
Black
}
const fristColor = Color[255]; // 只有使用字符串文本才能访问常数枚举成员。ts(2476)
const secondColor = Color[4]; // 只有使用字符串文本才能访问常数枚举成员。ts(2476)
可以看到,const enum不允许反向查找。并且使用const enum,默认也不生成任何JavaScript代码,而是在用到枚举成员的地方插入对应的值。
如果需要为
const enum生成运行时代码,在tsconfig.json中把TSC选项中的preserveConstEnums设为true:
"compilerOptions": {
"preserveConstEnums": true
},
小结
TypeScript内置了大量的类型。我们可以显示注解类型,也可以让TypeScript自动根据值推导出类型。使用const推导出来的类型更具体(除特殊情况),而let和var更一般化。