在学习之前,我有一个疑问:已经有了 JavaScript,为什么要有 TypeScript?先留个悬念,后面解答
原始类型:string、number 和 boolean
JavaScript 有三个非常常用的原始类型:string
、number
和 boolean
。 每个在 TypeScript 中都有对应的类型。 如您所料,如果您对这些类型的值使用 JavaScript typeof
运算符,这些名称与您看到的名称相同:
string 表示字符串值,如
"Hello, world"
number 代表像 42 这样的数字。JavaScript 对整数没有特殊的运行时值,因此没有等价于 int 或 float,一切都只是 number
boolean 代表
true
和false
这两个值
类型名称
String
、Number
和Boolean
(以大写字母开头)是合法的,但指的是一些很少出现在代码中的特殊内置类型。始终使用 string、number 或 boolean 作为类型。
数组
要指定像 [1, 2, 3] 这样的数组的类型,可以使用语法 number[]
;此语法适用于任何类型(例如 string[]
是字符串数组,等等)。 你也可以看到这个写成Array<number>
,意思是一样的。
请注意,
[number]
是另一回事;请参阅关于 元组 的部分。
any
TypeScript 也有一个特殊的类型,any
,当你不希望某个特定的值导致类型检查错误时,你可以使用它。
当一个值的类型为 any
时,您可以访问它的任何属性(这又将是 any
类型),像函数一样调用它,将它分配给(或从)任何类型的值,或者几乎任何其他东西这在语法上是合法的:
let obj: any = { x: 0 };
// None of the following lines of code will throw compiler errors.
// Using `any` disables all further type checking, and it is assumed
// you know the environment better than TypeScript.
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;
当你不想写出一个长类型来让 TypeScript 相信特定的代码行是可以的时,any
类型很有用。
noImplicitAny
当你没有指定类型,并且 TypeScript 不能从上下文推断它时,编译器通常会默认为 any
。
不过,您通常希望避免这种情况,因为 any
没有经过类型检查。 使用编译器标志 noImplicitAny
将任何隐式 any
标记为错误。
对象类型
除了原始类型之外,您会遇到的最常见的类型是对象类型。 这指的是任何带有属性的 JavaScript 值,几乎是所有属性! 要定义对象类型,我们只需列出其属性及其类型。
可选属性
对象类型还可以指定它们的部分或全部属性是可选的。 为此,请在属性名称后添加 ?
:
function printName(obj: { first: string; last?: string }) {
// ...
}
// Both OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });
在 JavaScript 中,如果您访问一个不存在的属性,您将获得值 undefined
而不是运行时错误。 因此,当您从可选属性中读取数据时,您必须在使用它之前检查 undefined
,两个参数之间可以使用,
或者;
隔开。
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 的类型系统允许您使用各种运算符从现有类型中构建新类型。 现在我们知道如何编写几种类型,是时候开始以有趣的方式组合它们了。
定义联合类型
您可能会看到的第一种组合类型的方法是联合类型。 联合类型是由两种或多种其他类型组成的类型,表示可能是这些类型中的任何一种的值。 我们将这些类型中的每一种都称为联合的成员。
让我们编写一个可以对字符串或数字进行操作的函数:
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
上可用的方法:
function printId(id: number | string) {
// Error
console.log(id.toUpperCase());
}
解决方案是用代码缩小联合,就像在没有类型注释的 JavaScript 中一样。 当 TypeScript 可以根据代码的结构为某个值推断出更具体的类型时,就会发生缩小。
例如,TypeScript 知道只有 string
值才会有 typeof
值 "string"
:
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
这样的函数:
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
方法。 如果联合中的每个成员都有一个共同的属性,则可以使用该属性而不会缩小类型:
// Return type is inferred as number[] | string
function getFirstThree(x: number[] | string) {
return x.slice(0, 3);
}
类型的联合似乎具有这些类型的属性的交集,这可能会令人困惑。 这不是偶然的——联合这个名字来源于类型论。 联合
number | string
是通过取每种类型的值的联合组成的。 请注意,给定两个具有关于每个集合的相应事实的集合,只有这些事实的交集适用于集合本身的并集。 例如,如果我们有一个房间里有戴帽子的高个子,而另一个房间里有戴帽子的说西班牙语的人,在组合这些房间后,我们对每个人的唯一了解就是他们必须戴帽子。
类型别名
我们一直通过直接在类型注释中编写对象类型和联合类型来使用它们。 这很方便,但通常希望多次使用同一个类型并用一个名称引用它。
类型别名就是这样 - 任何类型的名称。 类型别名的语法是:
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;
请注意,别名只是别名,您不能使用类型别名来创建相同类型的不同的或独特的 "versions"。 当您使用别名时,就好像您已经编写了别名类型。 换句话说,这段代码可能看起来非法,但根据 TypeScript 是可以的,因为这两种类型都是同一类型的别名:
declare function getInput(): string;
declare function sanitize(str: string): string;
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";
从上面的代码可以看出,定义了一个类型别名UserInputSanitizedString
,它就是 string 类型的别名,用于增加代码的可读性type UserInputSanitizedString = string;
但是这种有意义吗,有人认为直接使用 string 更简单明了,其实这种在特定情况下还是有很大用处的。
从纯粹的技术角度看,如果
UserInputSanitizedString
只是简单地别名string
,那么在类型表达上它并不提供额外的安全性或功能,因为最终它们都等同于基本的字符串类型。但是,类型别名在以下方面可以增加代码的可读性和维护性:
文档和意图:类型别名可以作为代码中的文档,明确指出某个类型代表的具体意义。在这个例子中,
UserInputSanitizedString
明确表示这是一个经过清理的、预期用于特定上下文(如用户输入处理)的字符串。这对于阅读代码的其他开发者来说是一个有用的提示。抽象和复用:在更复杂的项目中,类型别名可能不仅仅是一个简单的
string
。它可能代表一个具有特定结构的字符串(例如,使用正则表达式或格式要求的字符串),或者是一个联合类型、元组等。这样的别名便于在多处复用,且易于后续修改而不影响所有使用该类型的代码。易于未来的更改:即便一开始类型别名和基础类型相同,未来如果需求变化,需要对这个类型添加更多约束(比如使用字符串字面量类型、接口等),只需修改别名定义一处即可,无需改动所有使用该类型的代码位置。
所以,虽然在这个特定的简单示例中,类型别名可能看起来多余,但在大型项目或考虑长期维护和团队协作的背景下,它是一种提高代码清晰度和灵活性的有效方式。
类型别名和接口的区别
类型别名和接口非常相似,在很多情况下您可以在它们之间自由选择。 interface
的几乎所有功能都在 type
中可用,主要区别在于无法重新打开类型以添加新属性,而接口始终可扩展。
类型断言
有时你会得到关于 TypeScript 无法知道的值类型的信息。
例如,如果您使用的是 document.getElementById
,TypeScript 只知道这将返回某种 HTMLElement
,但您可能知道您的页面将始终具有具有给定 ID 的 HTMLCanvasElement
。
在这种情况下,您可以使用类型断言来指定更具体的类型:
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
与类型注释一样,类型断言被编译器删除,不会影响代码的运行时行为。
您还可以使用尖括号语法(除非代码在 .tsx
文件中),它是等效的:
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
提醒:因为类型断言在编译时被删除,所以没有与类型断言关联的运行时检查。 如果类型断言错误,则不会产生异常或
null
。
TypeScript 只允许类型断言转换为更具体或更不具体的类型版本。 此规则可防止impossible
强制,例如:
const x = "hello" as number;
有时,此规则可能过于保守,并且不允许可能有效的更复杂的强制转换。 如果发生这种情况,您可以使用两个断言,首先是any
或unknown
,然后是所需的类型:
declare const expr: any;
type T = { a: 1; b: 2; c: 3 };
const a = (expr as any) as T;
字面类型
除了通用类型 string
和 number
之外,我们还可以在类型位置引用特定的字符串和数字。
考虑这一点的一种方法是考虑 JavaScript 如何使用不同的方法来声明变量。var
和 let
都允许更改变量中保存的内容,而 const
不允许。这反映在 TypeScript 如何为字面创建类型。
来看看下面的代码:
let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";
变量只能有一个值并没有多大用处!
但是通过将字面组合成联合,就可以表达一个更有用的概念——例如,只接受一组已知值的函数:
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");
数字字面类型的工作方式相同:
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");
还有一种字面类型:布尔字面。 只有两种布尔字面类型,正如您可能猜到的,它们是 true
和 false
类型。 类型 boolean
本身实际上只是联合 true | false
的别名。
null 和 undefined
JavaScript 有两个原始值用于表示不存在或未初始化的值:null
和 undefined
。
TypeScript 有两个对应的同名类型。这些类型的行为取决于您是否启用了 strictNullChecks
选项。
非空断言运算符(后缀 !)
TypeScript 还具有一种特殊的语法,可以在不进行任何显式检查的情况下从类型中删除 null
和 undefined
。 在任何表达式之后写!
实际上是一个类型断言,该值不是 null
或 undefined
:
function liveDangerously(x?: number | null) {
// No error
console.log(x!.toFixed());
}
就像其他类型断言一样,这不会改变代码的运行时行为,所以当你知道值不能是null
或undefined
时,只使用!
很重要。
当尝试访问 null.toFixed() 时,JavaScript 运行时会抛出错误,因为 null 没有 toFixed 方法。
意义与风险:
意义:在你确切知道某个值在运行时不会为 null 或 undefined 的情况下,非空断言可以帮助你避免编写额外的 null 检查代码,使代码更加简洁。这对于那些经过仔细考虑且能确保安全性的场景是有帮助的。
风险:如果不慎使用,非空断言可能会导致运行时错误,就像上述示例所示。它要求开发者对代码逻辑有绝对的信心,否则可能会隐藏潜在的 null 引用错误,直到程序在用户面前崩溃。
因此,使用非空断言应当谨慎,确保在断言之前确实已经验证了变量不会为
null
或undefined
。
通过上面的介绍以及学习,现在回到刚开始的问题,为什么要学习 TypeScript ?
TypeScript 是 JavaScript 的一个超集,它在 JavaScript 的基础上添加了静态类型定义和其他一些功能,旨在解决大型应用程序开发中常见的问题。尽管 JavaScript 已经是一种非常流行的编程语言,但 TypeScript 的引入主要是为了提高代码的可维护性、可读性和开发效率,尤其是在团队合作和构建复杂应用时。以下是 TypeScript 相对于 JavaScript 的一些主要优势:
静态类型检查:TypeScript 最显著的特点是提供了静态类型系统。这允许开发者在编译阶段就发现类型错误,而不是在运行时,有助于提前捕捉错误并提高代码质量。
更好的开发工具支持:静态类型信息使得IDE(如Visual Studio Code)能够提供更加强大的代码补全、接口提示、重构支持等特性,从而提高开发效率。
易于维护和协作:在大型项目中,静态类型为团队成员之间提供了关于代码如何使用的明确文档,降低了维护成本,使得新成员更容易理解代码库。
兼容性:TypeScript 编译后的代码是纯 JavaScript,可以在任何支持 JavaScript 的环境中运行,无需修改现有基础设施。
面向对象编程的增强支持:虽然JavaScript也支持类和对象,但TypeScript在此基础上提供了更完整的面向对象编程特性,比如接口、类、泛型等,使得代码结构更加清晰,更易于管理和扩展。
提前预防潜在错误:通过类型注解,开发者可以在编码阶段就避免很多因变量类型不匹配导致的错误,减少了调试时间。
总的来说,尽管JavaScript本身已经非常强大且灵活,TypeScript通过增加类型安全性和提升开发体验,特别适合于构建大规模、长期维护的企业级应用。然而,对于小型项目或快速原型开发,直接使用JavaScript可能更为轻便。选择使用TypeScript还是JavaScript,很大程度上取决于项目的规模、团队的偏好以及对类型安全的需求。
评论区