TypeScript 学习笔记(3)—— 常用类型

139 阅读8分钟

除了为变量注解类型之外,类型还可用于其它地方。类型相关的内容由两部分构成:类型定义、类型的使用场景。

原始类型

原始类型有三种:字符串型 string,数字型 number,布尔型 boolean。相应的,有三种内置的包装对象类型:StringNumberBoolean

数组

数组类型有两种写法:T[]Array<T>;其中,T 是数组元素的类型。

any

对于类型为 any 的值,允许对它进行任何合乎语法的操作,如:访问任意属性(这些属性也是 any 类型)、作为函数调用、和任何其它类型的变量互相赋值,等等。

let obj: any;
obj.p; // any
obj();
obj = 1;
let n: number = obj;

如果不想让某个值在类型检测时抛出错误,可以使用 any。使用 any 可以越过类型检测,它假定用户比 TypeScript 更了解环境。如果用户想让 TypeScript 相信某一行代码是正确的,又不想书写很长的类型,可以使用 any

变量的类型注解(annotation)

TypeScript 中声明变量时,可以显式地指明变量类型,也就是类型注解。

let num: number = 1;

实际上,TypeScript 会自动对代码进行类型推断,比如:基于变量初始值推断类型。

函数

TypeScript 允许为函数的参数和返回值注解类型。

参数类型注解

声明函数时可以为每个参数注解类型;即使没有注解参数类型,TypeScript 也会检查参数的数目。

function test(n) {
  console.log(n);
}
test(1, 2); // error TS2554: Expected 1 arguments, but got 2.

返回值类型注解

紧接着参数列表的后面,可以为返回值注解类型;实际上,即使不添加返回值类型,TypeScript 也会根据 return 语句进行类型推断;但是,显式添加返回值类型,可在一定程度上避免代码的偶然变动。

匿名函数

使用匿名函数时,TypeScript 会根据所处的上下文来确定函数参数的类型,这被称为上下文类型推断(contextual typing)。

const names = ["tom", "Jerry", "Karl Max"];
names.forEach((name) => {
  // (parameter) name: string
  console.log(name.toUpperCase());
});

对象类型

定义对象类型只需列出属性以及相应的类型即可,属性之间使用 ',' 或 ';' 隔开,最后一个属性后面的分隔符可选;每个属性的类型部分都是可选的,如果没有指明,则默认为 any;要设置可选属性,只需在属性名和 ':' 之间加 '?' 即可,对于可选属性,使用时一般要检查是否为 undefined

function test(pt: { x: number; y; z?: string }) {}
// (parameter) pt: {
//     x: number;
//     y: any;
//     z?: string;
// }

联合类型

TypeScript 类型系统提供了各种各样的操作符,用于将已有的类型组合起来以构造新类型,联合类型就是其中之一。

联合类型由两个或更多的类型组合而成,表示这些类型中的某一个,这些类型被称为联合类型的成员,如:number | string

对于那些类型为联合类型的值,只有联合类型中每个成员都允许执行的操作,才允许执行。如果在代码中对联合类型收窄(narrowing),TypeScript 可以根据代码结构推断出更确切的类型,比如:typeof 运算符、以及 Array.isArray 等类似的判断类型的函数。

注意:

使用了联合符 '|' 不一定就是联合类型,如果参与联合的类型之间存在包含关系,最终的结果可能不是联合类型,如:

type union = 1 | number; // type union = number

此外,boolean 本身就是联合类型 true | false,虽然 boolean 中没有包含联合符 '|'。

类型别名

任何类型都可以起一个别名,方便重复使用。

type Point = {
  x: number;
  y: number;
};
type id = number | string;

别名只是为某一类型起了另一个名字,而不是为同一类型创建不同“版本”。

type otherString = string;
let s: otherString;
s = "1"; // let s: string

接口

接口声明是命名对象类型的一种方式。TypeScript 只关心类型的结构和功能(即:所期待的属性),因而被称为结构化类型系统。

接口与 type 非常相似,都可以表示对象类型,也都可以实现继承,大多情况下可以互相转换。

interface AnimalInterface {
  name: string;
}
interface BearInterface extends AnimalInterface {
  honey: boolean;
}
let bInterface: BearInterface = {
  name: "a",
  honey: true,
};

type AnimalType = {
  name: string;
};
type BearType = AnimalType & {
  honey: boolean;
};
let bType: BearType = {
  name: "b",
  honey: false,
};

两者间最主要的区别在于:

  1. type 无法合并声明,但接口可以;
  2. 接口仅用于声明对象的形状,但无法对类型重命名。
interface WindowInterface {
  title: string;
}
interface WindowInterface {
  id: number;
}
let s: WindowInterface = {
  title: "t",
  id: 1,
};

type WindowType = {
  // error TS2300: Duplicate identifier 'WindowType'.
  title: string;
};
type WindowType = {
  // error TS2300: Duplicate identifier 'WindowType'.
  id: number;
};

类型断言

对于某个值的类型,用户可能比 TypeScript 知道的更多。此时,可以使用类型断言来为该值指定更确切的类型。类型断言最终会被编译器移除,而不会影响代码的运行时行为。类型断言有两种语法:as<Type>,其中 Type 为数据类型。

let b: number | string;
const a = b as number; // const a: number
const c = <number>b; // const c: number

对于类型断言,TypeScript 只允许在有交集的类型间互相转化。这种限制可能会让某些运行时正确的类型转换在 TypeScript 编译时报错,此时可使用两次断言来实现类型转换:先转为 any/unknown(先放大),再转为想要的类型(后缩小)。

let a: number | boolean;
let b: string | boolean;
a = b as number | boolean;
let num: number;
let bool: boolean;
num = bool as number; // error TS2352: Conversion of type 'boolean' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
num = bool as unknown as number;

字面量类型

给定的字符串、数字、布尔值也可以作为类型来使用,这被称为字面量类型。

JavaScript 声明变量有三种方式:varletconstvarlet 声明的是变量,const 声明的是常量。TypeScript 中与 const 相对应的就是字面量类型。声明了字面量类型的变量只能取值为该字面量,const 关键字声明的变量自动被推断为字面量类型。

const constStr = "hello"; // const constStr: "hello"
let letStr = "hello"; // let letStr: string

字面量类型一般与联合类型结合起来,用于描述由一组特定值所构成的类型;字面量类型也可以和非字面量类型一起构成联合类型。

类型 boolean 就是字面量联合类型 true | false 的别名。

对象字面量推断

不管用什么关键字声明对象,都会隐式推断出相应的对象类型;但是,即使 const 声明,推断出的属性也都是可读可写的,因而被推断为较为宽松的类型,如原始数据类型,而不是字面量类型。这在对象属性作为参数传给较窄的类型时,可能会出现问题:

function handleReq(url, method: "get") {}
const req = { url: "www.", method: "get" };
// const req: {
//   url: string;
//   method: string;
// }
handleReq(req.url, req.method); // error TS2345: Argument of type 'string' is not assignable to parameter of type '"get"'.

下面两种方案可以解决这一问题:

  1. 断言属性类型

    (1)声明时断言,更改属性类型

    const req = { url: "www.", method: "get" as "get" };
    // const req: {
    //   url: string;
    //   method: "get";
    // }
    handleReq(req.url, req.method);
    

    (2)传参时断言

    const req = { url: "www.", method: "get" };
    handleReq(req.url, req.method as "get");
    
  2. as const 断言对象类型

    此时,对象属性均为只读字面量类型。

    const req = { url: "www.", method: "get" } as const;
    // const req: {
    //   readonly url: "www.";
    //   readonly method: "get";
    // }
    handleReq(req.url, req.method);
    req.method = "get"; // error TS2540: Cannot assign to 'method' because it is a read-only property.
    

    as const 是深度断言,即,深层嵌套的属性也会被断言为只读字面量类型。

    const obj = {
      a: {
        a1: "a1",
        a2: {
          a21: "a21",
        },
      },
      b: 2,
    } as const;
    // const obj: {
    //   readonly a: {
    //       readonly a1: "a1";
    //       readonly a2: {
    //           readonly a21: "a21";
    //       };
    //   };
    //   readonly b: 2;
    // }
    

nullundefined

TypeScript 为 nullundefined 提供了两种与自身同名的类型,标志 strictNullChecks 会影响这两种类型的行为。

strictNullChecks 关闭

nullundefined 是其它任何类型的子类型。

let n: number; // let n: number
n = null;
n = undefined;
// No error

无论使用哪个关键字声明,初始值为 null/undefined 的变量都会被隐式推断为 any 类型。

const a = undefined; // const a: any
const b = null; // const b: any

strictNullChecks 打开

类型 nullundefined 将与其它原始类型同级,不再有父子类型关系。

let n: number;
n = null; // error TS2322: Type 'null' is not assignable to type 'number'.
n = undefined; // error TS2322: Type 'undefined' is not assignable to type 'number'.

undefined 总是可以赋值给 void 类型,可能是因为 JavaScript 中函数无返回值时默认返回 undefined

let v: void;
v = undefined;
// No error

无论使用哪个关键字声明,初始值为 null/undefined 的变量都会隐式推断为类型 null/undefined

var a = undefined; // var a: undefined
let b = null; // let b: null

null 断言操作符:后缀 !

紧接着表达式后面书写 !,就是在断言该表达式的值既不是 null,也不是 undefined

function test(x?: number | null) {
  x!.toFixed();
  // No error
}

枚举类型 Enum

枚举类型不仅存在于类型层,同时也会影响运行时的行为

不太常用的原始类型

bigint

ES2020 之后,JavaScript 新增了一种原始数据类型 bigInt,用来表示大整数。

symbol

JavaScript 中 symbol 类型的值通过 Symbol() 函数进行创建,每个值都是全局唯一的。