读《TypeScript》总结——类型全解

868 阅读11分钟

类型术语

看下面这个函数:

function squareOf(n: number) {
    return n * n;
}

squareOf(2)
squareOf('z') // ts(2345): 类型“string”的参数不能赋给类型“number”的参数

显然,这个函数只能操作数字,如果传入数字以外的值,TypeScript将立即报错。这只是一个简单的例子,不过足以引入TypeScript类型的一些关键概念。从上述代码可以看出:

  1. squareOf的参数n被约束为number。
  2. 这个值可以赋值给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的用法:

  1. TypeScript不会把任何值推导为unknown类型,必须显示注解。
  2. unknown类型的值可以进行比较,否定,类型判断的操作
  3. 执行操作时,不能假定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:

  1. 可以让TypeScript推导出值的类型为boolean(a和b)
  2. 可以让TypeScript推导出值为某个具体的布尔值(c)
  3. 可以明确的告诉TypeScript,值的类型为boolean(d)
  4. 可以明确的告诉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类型也有四种方式:

  1. 可以让TypeScript推导出值的类型为number(a和b)
  2. 可以让TypeScript推导出值为某个具体的数字(c)
  3. 可以明确的告诉TypeScript,值的类型为number(e)
  4. 可以明确的告诉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)

通过这个示例得知:

  1. 使用const声明的符号,会被推导为 unique symbol类型,在代码编辑器中显示为 typeof yourVariableName,而不是unique symbol。
  2. 可以显示注解const变量的类型为unique symbol。
  3. unique symbol类型的变量必须为const。
  4. 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
};
  1. a 有个类型为string的属性name
  2. a 可能有个类型为number的属性age,如果有属性age,其值可以为undefined
  3. 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中,声明对象类型有四种方式:

  1. object类型。如果需要一个对象,但对对象的字段没有要求,可以使用这种方式。
  2. 自动推导。让TypeScript自动推导出对象的类型。
  3. 对象字面量句法。如果我们知道对象有哪些字段,可以使用这种方式。
  4. 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更一般化。