TS文档学习 --- 常见类型(下)

256 阅读6分钟

本篇翻译整理自 TypeScript Handbook 中 「Everyday Types」 章节。

类型别名(Type Aliases)

有的时候,一个类型会被使用多次,此时我们更希望通过一个单独的名字来引用它。

这就是类型别名(type alias)。所谓类型别名,顾名思义,一个可以指代任意类型的名字。

类型别名的语法是使用type关键字

type ID = number | string;

当你使用类型别名的时候,它就跟你编写的类型是一样的

type UserInputSanitizedString = string;

const userInput: UserInputSanitizedString = "new input";

接口(Interfaces)

接口声明(interface declaration)是命名对象类型的另一种方式

interface Point {
  x: number;
  y: number;
}
 
function printCoord(pt: Point) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}
 
printCoord({ x: 100, y: 100 });

TypeScript 只关心传递给 printCoord 的值的结构(structure)——关心值是否有期望的属性。正是这种只关心类型的结构和能力的特性,我们才认为 TypeScript 是一个结构化(structurally)的类型系统。

类型别名和接口的不同

类型别名和接口非常相似,大部分时候,你可以任意选择使用

接口的几乎所有特性都可以在 type 中使用

// Interface
// 通过继承扩展类型
interface Animal {
  name: string
}

interface Bear extends Animal {
  honey: boolean
}

const bear = getBear() 
bear.name
bear.honey

// --------------------------------------
        
// Type
// 通过交集扩展类型
type Animal = {
  name: string
}

type Bear = Animal & { 
  honey: boolean 
}

const bear = getBear();
bear.name;
bear.honey;

两者区别在于:

  • 类型别名也许不会实现声明合并,但是接口可以
// An interface can be re-opened
// and new values added:

interface Mammal {
  genus: string
}

interface Mammal {
  breed?: string
}

const animal: Mammal = {
  genus: "1234",
  // Fails because breed has to be a string
  breed: 1
}

type Reptile = {
  genus: string
}

// You cannot add new variables in the same way
type Reptile = {
  breed?: string
}
  • 接口可能只会被用于声明对象的形状,不能重命名原始类型
// Here's two examples of 
// using types and interfaces
// to describe an object 

interface AnObject1 {
    value: string
}

type AnObject2 = {
    value: string
}

// Using type we can create custom names
// for existing primitives:

type SanitizedString = string
type EvenNumber = number

// This isn't feasible with interfaces
interface X extends string { }

类型断言(Type Assertions)

有的时候,你知道一个值的类型,但 TypeScript 不知道

举个例子,如果你使用 document.getElementById,TypeScript 仅仅知道它会返回一个 HTMLElement,但是你却知道,你要获取的是一个 HTMLCanvasElement

这时,你可以使用类型断言将其指定为一个更具体的类型

方式1 --- as语法

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

方式2 --- 尖括号语法 --- tsx中不支持

const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");

就像类型注解一样,类型断言也会被编译器移除,并且不会影响任何运行时的行为。

双重断言

默认情况下,TypeScript 仅仅允许类型断言转换为一个更加具体或者更不具体的类型

有的时候,这条规则会显得非常保守,阻止了你原本有效的类型转换。

如果发生了这种事情,你可以使用双重断言,先断言为 any (或者是 unknown),然后再断言为期望的类型:

const x = "hello" as unknown as number;

字面量类型(Literal Types)

除了常见的类型 stringnumber ,我们也可以将类型声明为更具体的数字或者字符串

当一个变量的是使用const进行类型声明的时候,声明的变量则不能被修改,这就会影响 TypeScript 为字面量创建类型。

const str = "Hello World";
//     ^? str的类型为 'Hello World'

字面量类型本身并没有什么太大用, 但如果结合联合类型,就显得有用多了。

function printText(s: string, alignment: "left" | "right" | "center") {
  // ...
}

printText("Hello, world", "left"); // success
printText("G'day, mate", "centre"); // error

同样我们可以创建数字字面量

function compare(a: string, b: string): -1 | 0 | 1 {
  return a === b ? 0 : a > b ? 1 : -1;
}

字面量类型也可以和非字面量类型联合一起使用

interface Options {
  width: number;
}

function configure(x: Options | "auto") {
  // ...
}

configure({ width: 100 }); // success
configure("auto"); // success
configure("automatic"); // error

布尔字面量本质上是一个特殊的字面量类型。因为只有两种布尔字面量类型, truefalse

类型 boolean 实际上就是联合类型 true | false 的别名。

字面量推断(Literal Inference)

当你初始化变量为一个对象的时候,TypeScript 会假设这个对象的属性的值未来会被修

const obj = { counter: 0 };

if (someCondition) {
  obj.counter = 1; // obj.counter会被推测为number类型,而不会被推测为0
}

这样在实际使用的时候,会导致一些问题

declare function handleRequest(url: string, method: "GET" | "POST"): void;

const req = { url: "https://example.com", method: "GET" };

// req的url属性和method属性会被推测为string
// 因为在创建 req 和 调用 handleRequest 函数之间,可能还有其他的代码,或许会将 req.method 赋值一个新字符串比如 "Guess" 
// 而handleRequest的参数method需要"GET" | "POST"
// 此时就会报错
handleRequest(req.url, req.method); // error

解决方式如下:

  1. 类型断言
// Change 1: --- 有意让 req.method 的类型为字面量类型 "GET",这会阻止未来可能赋值为 "GUESS" 等字段”
const req = { url: "https://example.com", method: "GET" as "GET" };

// Change 2 --- 我知道 req.method 的值是 "GET"
handleRequest(req.url, req.method as "GET");
  1. 转换为类型字面量
// as const 效果跟 const 类似,但是对类型系统而言,
// 它可以确保所有的属性都被赋予一个字面量类型,而不是一个更通用的类型比如 string 或者 number 
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);

nullundefined

nullundefined默认是其它任意类型的子类型,可以别赋值给其它任意类型变量。

但这往往是bug的源头,所以我们始终推荐开发者开启 strictNullChecks 选项。

当开启 strictNullChecks 选项后,在使用值可能为nullundefined的属性或方法的时候

需要先判断一下是否为nullundefined后,在执行其它处理逻辑

非空断言操作符(后缀 !)(Non-null Assertion Operator)

TypeScript 提供了一个特殊的语法,可以在不做任何检查的情况下,从类型中移除 nullundefined

这就是在任意表达式后面写上 ! ,这是一个有效的类型断言,表示它的值不可能是 null 或者 undefined

就像其他的类型断言,非空断言也不会更改任何运行时的行为

function liveDangerously(x?: number | null) {
  // x! 表示 x肯定不为null
  console.log(x!.toFixed());
}

枚举(Enums)

枚举是 TypeScript 添加的新特性,用于给多个常量值一个方便使用的别名

不同于大部分的 TypeScript 特性,枚举并不是一个类型层面的增量,而是会添加到语言和运行时

枚举的具体使用规则可以查看枚举类型页面了解更多信息

不常见的原始类型(Less Common Primitives)

TypeScript中同样支持JavaScript中一些不常用的数据类型

bigInt

ES2020 引入原始类型 BigInt,用于表示非常大的整数

// 通过BigInt函数创建一个bigint类型数据
const oneHundred: bigint = BigInt(100);
 
// 通过字面量类型创建一个bigint类型数据
const anotherHundred: bigint = 100n;

symbol

这也是 JavaScript 中的一个原始类型,通过函数 Symbol(),我们可以创建一个全局唯一的引用

const firstName = Symbol("name");
const secondName = Symbol("name");
 
// error
// firstName === secondName的值恒为false
if (firstName === secondName) {
  // ...
}