mqyqingfeng / Blog

冴羽写博客的地方,预计写四个系列:JavaScript深入系列、JavaScript专题系列、ES6系列、React系列。
30.53k stars 4.7k forks source link

TypeScript 之常见类型(下) #229

Open mqyqingfeng opened 2 years ago

mqyqingfeng commented 2 years ago

TypeScript 的官方文档早已更新,但我能找到的中文文档都还停留在比较老的版本。所以对其中新增以及修订较多的一些章节进行了翻译整理。

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

本文并不严格按照原文翻译,对部分内容也做了解释补充。

类型别名(Type Aliases)

我们已经学会在类型注解里直接使用对象类型和联合类型,这很方便,但有的时候,一个类型会被使用多次,此时我们更希望通过一个单独的名字来引用它。

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

type Point = {
  x: number;
  y: number;
};

// Exactly the same as the earlier example
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 });

你可以使用类型别名给任意类型一个名字,举个例子,命名一个联合类型:

type ID = number | string;

注意别名是唯一的别名,你不能使用类型别名创建同一个类型的不同版本。当你使用类型别名的时候,它就跟你编写的类型是一样的。换句话说,代码看起来可能不合法,但对 TypeScript 依然是合法的,因为两个类型都是同一个类型的别名:

type UserInputSanitizedString = string;

function sanitizeInput(str: string): UserInputSanitizedString {
  return sanitize(str);
}

// Create a sanitized input
let userInput = sanitizeInput(getInput());

// Can still be re-assigned with a string though
userInput = "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;
// Interface
// 对一个已经存在的接口添加新的字段
interface Window {
  title: string
}

interface Window {
  ts: TypeScriptAPI
}

const src = 'const a = "Hello World"';
window.ts.transpileModule(src, {});

// Type
// 创建后不能被改变
type Window = {
  title: string
}

type Window = {
  ts: TypeScriptAPI
}

// Error: Duplicate identifier 'Window'.

在后续的章节里,你还会了解的更多。所以下面这些内容不能立刻理解也没有关系:

大部分时候,你可以根据个人喜好进行选择。TypeScript 会告诉你它是否需要其他方式的声明。如果你喜欢探索性的使用,那就使用 interface ,直到你需要用到 type 的特性。

类型断言(Type Assertions)

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

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

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

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

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

你也可以使用尖括号语法(注意不能在 .tsx 文件内使用),是等价的:

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

谨记:因为类型断言会在编译的时候被移除,所以运行时并不会有类型断言的检查,即使类型断言是错误的,也不会有异常或者 null 产生。

TypeScript 仅仅允许类型断言转换为一个更加具体或者更不具体的类型。这个规则可以阻止一些不可能的类型指定,比如:

const x = "hello" as number;
// Conversion of type 'string' 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.

有的时候,这条规则会显得非常保守,阻止了你原本有效的类型转换。如果发生了这种事情,你可以使用双重断言,先断言为 any (或者是 unknown),然后再断言为期望的类型:

const a = (expr as any) as T;

字面量类型(Literal Types)

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

众所周知,在 JavaScript 中,有多种方式可以声明变量。比如 varlet ,这种方式声明的变量后续可以被修改,还有 const ,这种方式声明的变量则不能被修改,这就会影响 TypeScript 为字面量创建类型。

let changingString = "Hello World";
changingString = "Olá Mundo";
// Because `changingString` can represent any possible string, that
// is how TypeScript describes it in the type system
changingString;
// let changingString: string
const constantString = "Hello World";
// Because `constantString` can only represent 1 possible string, it
// has a literal type representation
constantString;
// const constantString: "Hello World"

字面量类型本身并没有什么太大用:

let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";
// Type '"howdy"' is not assignable to type '"hello"'.

如果结合联合类型,就显得有用多了。举个例子,当函数只能传入一些固定的字符串时:

function printText(s: string, alignment: "left" | "right" | "center") {
  // ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");
// Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.

数字字面量类型也是一样的:

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 });
configure("auto");
configure("automatic");

// Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.

还有一种字面量类型,布尔字面量。因为只有两种布尔字面量类型, truefalse ,类型 boolean 实际上就是联合类型 true | false 的别名。

字面量推断(Literal Inference)

当你初始化变量为一个对象的时候,TypeScript 会假设这个对象的属性的值未来会被修改,举个例子,如果你写下这样的代码:

const obj = { counter: 0 };
if (someCondition) {
  obj.counter = 1;
}

TypeScript 并不会认为 obj.counter 之前是 0, 现在被赋值为 1 是一个错误。换句话说,obj.counter 必须是 number 类型,但不要求一定是 0,因为类型可以决定读写行为。

这也同样应用于字符串:

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

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

// Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.

在上面这个例子里,req.method 被推断为 string ,而不是 "GET",因为在创建 req 和 调用 handleRequest 函数之间,可能还有其他的代码,或许会将 req.method 赋值一个新字符串比如 "Guess" 。所以 TypeScript 就报错了。

有两种方式可以解决:

  1. 添加一个类型断言改变推断结果:
// Change 1:
const req = { url: "https://example.com", method: "GET" as "GET" };
// Change 2
handleRequest(req.url, req.method as "GET");

修改 1 表示“我有意让 req.method 的类型为字面量类型 "GET",这会阻止未来可能赋值为 "GUESS" 等字段”。修改 2 表示“我知道 req.method 的值是 "GET"”.

  1. 你也可以使用 as const 把整个对象转为一个类型字面量:
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);

as const 效果跟 const 类似,但是对类型系统而言,它可以确保所有的属性都被赋予一个字面量类型,而不是一个更通用的类型比如 string 或者 number

nullundefined

JavaScript 有两个原始类型的值,用于表示空缺或者未初始化,他们分别是 nullundefined

TypeScript 有两个对应的同名类型。它们的行为取决于是否打开了 strictNullChecks 选项。

strictNullChecks 关闭

strictNullChecks 选项关闭的时候,如果一个值可能是 null 或者 undefined,它依然可以被正确的访问,或者被赋值给任意类型的属性。这有点类似于没有空值检查的语言 (比如 C# ,Java) 。这些检查的缺少,是导致 bug 的主要源头,所以我们始终推荐开发者开启 strictNullChecks 选项。

strictNullChecks 打开

strictNullChecks 选项打开的时候,如果一个值可能是 null 或者 undefined,你需要在用它的方法或者属性之前,先检查这些值,就像用可选的属性之前,先检查一下 是否是 undefined ,我们也可以使用类型收窄(narrowing)检查值是否是 null

function doSomething(x: string | null) {
  if (x === null) {
    // do nothing
  } else {
    console.log("Hello, " + x.toUpperCase());
  }
}

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

TypeScript 提供了一个特殊的语法,可以在不做任何检查的情况下,从类型中移除 nullundefined,这就是在任意表达式后面写上 ! ,这是一个有效的类型断言,表示它的值不可能是 null 或者 undefined

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

就像其他的类型断言,这也不会更改任何运行时的行为。重要的事情说一遍,只有当你明确的知道这个值不可能是 null 或者 undefined 时才使用 !

枚举(Enums)

枚举是 TypeScript 添加的新特性,用于描述一个值可能是多个常量中的一个。不同于大部分的 TypeScript 特性,这并不是一个类型层面的增量,而是会添加到语言和运行时。因为如此,你应该了解下这个特性。但是可以等一等再用,除非你确定要使用它。你可以在枚举类型页面了解更多的信息。

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

我们提一下在 JavaScript 中剩余的一些原始类型。但是我们并不会深入讲解。

bigInt

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

// Creating a bigint via the BigInt function
const oneHundred: bigint = BigInt(100);

// Creating a BigInt via the literal syntax
const anotherHundred: bigint = 100n;

你可以在 [TypeScript 3.2 的发布日志](the TypeScript 3.2 release notes)中了解更多信息。

symbol

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

const firstName = Symbol("name");
const secondName = Symbol("name");

if (firstName === secondName) {
  // This condition will always return 'false' since the types 'typeof firstName' and 'typeof secondName' have no overlap.
  // Can't ever happen
}

你可以在 Symbol 页面了解更多的信息。

TypeScript 系列

TypeScript 系列文章由官方文档翻译、重难点解析、实战技巧三个部分组成,涵盖入门、进阶、实战,旨在为你提供一个系统学习 TS 的教程,全系列预计 40 篇左右。点此浏览全系列文章,并建议顺便收藏站点。

微信:「mqyqingfeng」,加我进冴羽唯一的读者群。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

NameWjp commented 2 years ago

TypeScript 并不会认为 obj.counter 之前是 0, 现在被赋值为 1 是一个错误。换句话说,obj.counter 必须是 string 类型,但不要求一定是 0,因为类型可以决定读写行为。 这里的 obj.counter 必须是 string 类型 是不是应该为 obj.counter 必须是 number 类型

bigbigDreamer commented 2 years ago
enum Methods {
    'GET',
    'POST'
}

declare function handleRequest(url: string, method: keyof typeof Methods): void;

type ReqShape = {
    url: string;
    method: keyof typeof Methods;
}

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

抛开本节主题来讲,字面量推断的例子中,第二个例子,其实也可以这么干,让类型更通用。

zhang13pro commented 2 years ago
mqyqingfeng commented 2 years ago

@NameWjp 感谢勘误~ 确实是写错了,已更正~

mqyqingfeng commented 2 years ago

@bigbigDreamer 是的,感谢补充~

mqyqingfeng commented 2 years ago

@zhang13pro 哈哈,划重点了~

flasco commented 2 years ago

你可以在 [TypeScript 3.2 的发布日志](the TypeScript 3.2 release notes)中了解更多信息。这个超链接没标上~

Lanbasara commented 2 years ago

"注意别名是唯一的别名,你不能使用类型别名创建同一个类型的不同版本。当你使用类型别名的时候,它就跟你编写的类型是一样的。换句话说,代码看起来可能不合法,但对 TypeScript 依然是合法的,因为两个类型都是同一个类型的别名" 请教一下这段话的具体意思, 我没太理解完全。 看下面的例子, 感觉UserInputSanitizedString和string是完全一样的东西, 这不就是《同一个类型的不同版本》吗?

li-changyu commented 2 years ago

捉个虫,这一行的链接引用错误了

类型别名也许不会实现[声明合并,但是接口可以](https://www.typescriptlang.org/play?#code/PTAEGEHsFsAcEsA2BTATqNrLusgzngIYDm+oA7koqIYuYQJ56gCueyoAUCKAC4AWHAHaFcoSADMaQ0PCG80EwgGNkALk6c5C1EtWgAsqOi1QAb06groEbjWg8vVHOKcAvpokshy3vEgyyMr8kEbQJogAFND2YREAlOaW1soBeJAoAHSIkMTRmbbI8e6aPMiZxJmgACqCGKhY6ABGyDnkFFQ0dIzMbBwCwqIccabcYLyQoKjIEmh8kwN8DLAc5PzwwbLMyAAeK77IACYaQSEjUWZWhfYAjABMAMwALA+gbsVjoADqgjKESytQPxCHghAByXigYgBfr8LAsYj8aQMUASbDQcRSExCeCwFiIQh+AKfAYyBiQFgOPyIaikSGLQo0Zj-aazaY+dSaXjLDgAGXgAC9CKhDqAALxJaw2Ib2RzOISuDycLw+ImBYKQflCkWRRD2LXCw6JCxS1JCdJZHJ5RAFIbFJU8ADKC3WzEcnVZaGYE1ABpFnFOmsFhsil2uoHuzwArO9SmAAEIsSFrZB-GgAjjA5gtVN8VCEc1o1C4Q4AGlR2AwO1EsBQoAAbvB-gJ4HhPgB5aDwem-Ph1TCV3AEEirTp4ELtRbTPD4vwKjOfAuioSQHuDXBcnmgACC+eCONFEs73YAPGGZVT5cRyyhiHh7AAON7lsG3vBggB8XGV3l8-nVISOgghxoLq9i7io-AHsayRWGaFrlFauq2rg9qaIGQHwCBqChtKdgRo8TxRjeyB3o+7xAA)
ddddd727 commented 1 year ago

这里有一个翻译的小问题,这句:“注意别名是唯一的别名” 里面only翻译有误

注意别名是唯一的别名,你不能使用类型别名创建同一个类型的不同版本。当你使用类型别名的时候,它就跟你编写的类型是一样的。换句话说,代码看起来可能不合法,但对` TypeScript 依然是合法的,因为两个类型都是同一个类型的别名:

官方写的是:

Note that aliases are only aliases - you cannot use type aliases to create different/distinct “versions” of the same type. When you use the alias, it’s exactly as if you had written the aliased type. In other words, this code might look illegal, but is OK according to TypeScript because both types are aliases for the same type:

官方想表达的意思应该是:别名只是一个别名,你不能用类型别名来创造一个类型

望采纳

caoxiemeihao commented 1 year ago
- 你也可以使用尖括号语法(注意不能在 .tsx 文件内使用),是等价的:
+ 你也可以使用尖括号语法(注意不是在 .tsx 文件内使用),是等价的: