TypeScript学习之日常类型

函数

函数是在 JavaScript 中传递数据的主要方式。 TypeScript 允许您指定函数的输入和输出值的类型。

参数类型注解

声明函数时,可以在每个参数后面加上类型注解,声明函数接受哪些类型的参数。 参数类型注释在参数名称之后:

1
2
3
4
5
// Parameter type annotation
function greet(name: string) {
// ^^^^^^^^
console.log("Hello, " + name.toUpperCase() + "!!");
}

当参数具有类型注释时,将检查该函数的参数:

1
2
3
4
declare function greet(name: string): void;

// Would be a runtime error if executed!
greet(42);

即使您的参数上没有类型注释,TypeScript 仍会检查您是否传递了正确数量的参数。

返回类型注解

您还可以添加返回类型注释。 返回类型注释出现在参数列表之后:

1
2
3
4
function getFavoriteNumber(): number {
// ^^^^^^^^
return 26;
}

与变量类型注解非常相似,您通常不需要返回类型注解,因为 TypeScript 会根据其 return 语句推断函数的返回类型。 上面例子中的类型注解并没有改变任何东西。 一些代码库将明确指定返回类型以用于文档目的,以防止意外更改,或仅出于个人喜好。

匿名函数

匿名函数与函数声明有点不同。 当一个函数出现在 TypeScript 可以确定如何调用它的地方时,该函数的参数会自动被赋予类型。

这是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
// No type annotations here, but TypeScript can spot the bug
const names = ["Alice", "Bob", "Eve"];

// Contextual typing for function
names.forEach(function (s) {
console.log(s.toUppercase());
});

// Contextual typing also applies to arrow functions
names.forEach((s) => {
console.log(s.toUppercase());
});

即使参数 s 没有类型注释,TypeScript 还是使用 forEach 函数的类型以及推断的数组类型来确定 s 将具有的类型。

这个过程称为上下文类型,因为函数发生的上下文告知它应该具有什么类型。

与推理规则类似,您不需要明确了解这是如何发生的,但了解它确实发生可以帮助您注意到何时不需要类型注释。 稍后,我们将看到更多关于值出现的上下文如何影响其类型的示例。

对象类型

除了基本类型之外,您会遇到的最常见的类型是对象类型。 这指的是任何带有属性的 JavaScript 值,几乎是所有属性! 要定义对象类型,我们只需列出其属性及其类型。

例如,这是一个接受点状对象的函数:

1
2
3
4
5
6
7
// The parameter's type annotation is an object type
function printCoord(pt: { x: number; y: number }) {
// ^^^^^^^^^^^^^^^^^^^^^^^^
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });

在这里,我们使用具有两个属性的类型注释参数 - xy - 这两个属性都是 number 类型。 您可以使用 ,; 来分隔属性,最后一个分隔符是可选的。

每个属性的类型部分也是可选的。 如果不指定类型,则假定为 any

可选属性

对象类型还可以指定它们的部分或全部属性是可选的。 为此,请在属性名称后添加 ?

1
2
3
4
5
6
function printName(obj: { first: string; last?: string }) {
// ...
}
// Both OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });

在 JavaScript 中,如果您访问一个不存在的属性,您将获得值 undefined 而不是运行时错误。 因此,当您从可选属性中读取数据时,您必须在使用它之前检查 undefined

1
2
3
4
5
6
7
8
9
10
11
function printName(obj: { first: string; last?: string }) {
// Error - might crash if 'obj.last' wasn't provided!
console.log(obj.last.toUpperCase());
if (obj.last !== undefined) {
// OK
console.log(obj.last.toUpperCase());
}

// A safe alternative using modern JavaScript syntax:
console.log(obj.last?.toUpperCase());
}

联合类型

TypeScript 的类型系统允许您使用各种运算符从现有类型中构建新类型。 现在我们知道如何编写几种类型,是时候开始以有趣的方式组合它们了。

定义联合类型

您可能会看到的第一种组合类型的方法是联合类型。 联合类型是由两种或多种其他类型组成的类型,表示可能是这些类型中的任何一种的值。 我们将这些类型中的每一种都称为联合的成员。

让我们编写一个可以对字符串或数字进行操作的函数:

1
2
3
4
5
6
7
8
9
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
printId({ myID: 22342 });

使用联合类型

提供与联合类型匹配的值很容易 - 只需提供与联合的任何成员匹配的类型即可。 如果你有一个联合类型的值,你如何处理它?

TypeScript 只有在对联合的每个成员都有效的情况下才允许操作。 例如,如果您有联合 string | number,则不能使用仅在 string 上可用的方法:

1
2
3
function printId(id: number | string) {
console.log(id.toUpperCase());
}

解决方案是用代码缩小联合,就像在没有类型注释的 JavaScript 中一样。 当 TypeScript 可以根据代码的结构为某个值推断出更具体的类型时,就会发生缩小。

例如,TypeScript 知道只有 string 值才会有 typeof"string"

1
2
3
4
5
6
7
8
9
function printId(id: number | string) {
if (typeof id === "string") {
// In this branch, id is of type 'string'
console.log(id.toUpperCase());
} else {
// Here, id is of type 'number'
console.log(id);
}
}

另一个例子是使用像 Array.isArray 这样的函数:

1
2
3
4
5
6
7
8
9
function welcomePeople(x: string[] | string) {
if (Array.isArray(x)) {
// Here: 'x' is 'string[]'
console.log("Hello, " + x.join(" and "));
} else {
// Here: 'x' is 'string'
console.log("Welcome lone traveler " + x);
}
}

请注意,在 else 分支中,我们不需要做任何特别的事情——如果 x 不是 string[],那么它一定是 string

有时你会有一个联合,所有成员都有共同点。 例如,数组和字符串都有一个 slice 方法。 如果联合中的每个成员都有一个共同的属性,则可以使用该属性而不会缩小类型:

1
2
3
4
// Return type is inferred as number[] | string
function getFirstThree(x: number[] | string) {
return x.slice(0, 3);
}

类型的联合似乎具有这些类型的属性的交集,这可能会令人困惑。 这不是偶然的——联合这个名字来源于类型论。 联合 number | string 是通过取每种类型的值的联合组成的。 请注意,给定两个具有关于每个集合的相应事实的集合,只有这些事实的交集适用于集合本身的并集。 例如,如果我们有一个房间里有戴帽子的高个子,而另一个房间里有戴帽子的说西班牙语的人,在组合这些房间后,我们对每个人的唯一了解就是他们必须戴帽子。

实现一个 Swift 中的 compactMap 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 函数 compactMap
*
* 该函数会移除输入数组中的 null 和 undefined 元素,
* 然后返回新的数组,新数组中的元素类型和输入数组的非空元素类型一致。
*
* @param {Array<T | null | undefined>} array - 输入数组,元素可能为 T,null 或 undefined。
*
* @returns {Array<T>} 结果数组,包含和输入数组的非空元素相同类型的元素。
*
* @template T - 输入数组中非空元素的类型。
*/
function compactMap<T>(array: Array<T | null | undefined>): Array<T> {
// Filter: 去除 null 和 undefined,保留 T。
return array.filter((item): item is T => item != null);
}

let array = [1, null, 2, "hello", undefined, true, false];
let compacted = compactMap(array);
console.log(compacted); // 输出: [1, 2, "hello", true, false]

类型断言

有时你会得到关于 TypeScript 无法知道的值类型的信息。

例如,如果您使用的是 document.getElementById,TypeScript 只知道这将返回某种 HTMLElement,但您可能知道您的页面将始终具有具有给定 ID 的 HTMLCanvasElement

在这种情况下,您可以使用类型断言来指定更具体的类型:

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

与类型注释一样,类型断言被编译器删除,不会影响代码的运行时行为。

您还可以使用尖括号语法(除非代码在 .tsx 文件中),它是等效的:

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

提醒:因为类型断言在编译时被删除,所以没有与类型断言关联的运行时检查。 如果类型断言错误,则不会产生异常或 null

TypeScript 只允许类型断言转换为更具体或更不具体的类型版本。 此规则可防止 “impossible” 强制,例如:

1
const x = "hello" as number;

有时,此规则可能过于保守,并且不允许可能有效的更复杂的强制转换。 如果发生这种情况,您可以使用两个断言,首先是 any(或 unknown,我们稍后会介绍),然后是所需的类型:

1
2
3
4
declare const expr: any;
type T = { a: 1; b: 2; c: 3 };

const a = (expr as any) as T;