第1章:TypeScript入门(Getting Started with TypeScript)
一、导读
1.1 本章学习目标
第1章是《Programming TypeScript》的开篇,带你从零开始理解 TypeScript 的核心价值。TypeScript 不是另一门编程语言——它是 JavaScript 的超集,为 JavaScript 添加了静态类型系统。本章将帮助你理解 TypeScript 的设计哲学、搭建开发环境,并编写第一个 TypeScript 程序。
本章的核心学习目标包括:
- 理解 TypeScript 是什么:它解决了 JavaScript 的哪些问题?
- 掌握 TypeScript 的类型推断机制:大多数时候你不需要显式标注类型
- 搭建 TypeScript 开发环境:安装 TypeScript 编译器、配置
tsconfig.json - 理解编译选项:
strict、noImplicitAny、target等关键配置 - 编写第一个 TypeScript 程序:从类型注解到编译运行
- 理解 TypeScript 与 JavaScript 的关系:渐进式类型化、类型擦除
1.2 为什么第一章如此重要
TypeScript 的核心价值在于:它让你在编写代码时就能发现错误,而不是在运行时。一个没有类型标注的 JavaScript 项目,你可能要到用户报告 bug 时才发现 undefined is not a function。而 TypeScript 的编译器会在你写代码的那一刻就告诉你类型不匹配。
但 TypeScript 的哲学不是"强制你写更多类型"——它的类型推断非常强大,大多数时候编译器能从上下文推断出正确的类型。这意味着你可以渐进式地引入类型——从简单的类型注解开始,逐步增加类型的精确度。
1.3 核心问题引导
TypeScript 的"类型"和 Java/C# 的"类型"有什么本质区别?
为什么说 TypeScript 是 JavaScript 的"超集"而不是"替代品"?
strict: true 到底开启了哪些检查?为什么推荐开启?
类型推断在什么情况下会失效?什么时候需要显式标注类型?
TypeScript 的类型信息在运行时会消失吗?这对性能有什么影响?
二、核心概念详解
2.1 TypeScript 的设计哲学
2.1.1 渐进式类型化
TypeScript 的核心设计理念是渐进式类型化(gradual typing)。你可以从一个完全没有类型的 JavaScript 文件开始,逐步添加类型注解:
// 阶段1:纯 JavaScript
function add(a, b) {
return a + b;
}
// 阶段2:添加基本类型
function add(a: number, b: number): number {
return a + b;
}
// 阶段3:使用接口
interface Addable {
add(a: number, b: number): number;
}
class Calculator implements Addable {
add(a: number, b: number): number {
return a + b;
}
}2.1.2 结构化类型系统
TypeScript 使用结构化类型系统(structural typing),而不是 Java/C# 的名义类型系统(nominal typing)。这意味着类型的兼容性由结构决定,而不是由名称决定:
interface Point2D {
x: number;
y: number;
}
interface Point3D {
x: number;
y: number;
z: number;
}
const p3d: Point3D = { x: 1, y: 2, z: 3 };
const p2d: Point2D = p3d; // OK! Point3D 有 Point2D 需要的所有属性
// 在 Java 中,Point3D 不是 Point2D 的子类型(除非显式 extends)
// 在 TypeScript 中,只要结构兼容就可以赋值2.2 基本类型
2.2.1 原始类型
let name: string = "TypeScript";
let age: number = 5;
let isAwesome: boolean = true;
let nothing: null = null;
let notDefined: undefined = undefined;
// bigint 和 symbol
let bigNum: bigint = 9007199254740991n;
let sym: symbol = Symbol("unique");2.2.2 数组与元组
// 数组
let numbers: number[] = [1, 2, 3];
let strings: Array<string> = ["a", "b", "c"]; // 泛型写法
// 元组:固定长度和类型的数组
let pair: [string, number] = ["age", 25];
let [key, value] = pair; // 解构
// 可选元组元素
let optional: [string, number?] = ["name"];2.2.3 对象类型
// 内联对象类型
let user: { name: string; age: number } = {
name: "Alice",
age: 30
};
// 使用接口
interface User {
name: string;
age: number;
email?: string; // 可选属性
}
let alice: User = {
name: "Alice",
age: 30
};2.3 类型推断
TypeScript 的类型推断非常强大,大多数时候不需要显式标注类型:
// 自动推断为 number
let count = 42;
// 自动推断为 string
let message = "Hello";
// 自动推断为 (a: number, b: number) => number
let add = (a: number, b: number) => a + b;
// 自动推断为 string[]
let names = ["Alice", "Bob", "Charlie"];
// 上下文推断
const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2); // n 自动推断为 number2.3.1 最佳公共类型
// 编译器会选择最宽泛的公共类型
let arr = [1, "hello", true]; // (string | number | boolean)[]
// 空数组推断为 any[]
let empty = []; // any[]
let emptyTyped: string[] = []; // string[]2.4 配置 TypeScript
2.4.1 tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}2.4.2 strict 模式
strict: true 开启了以下检查:
noImplicitAny:不允许隐式any类型strictNullChecks:null和undefined不能赋值给其他类型strictFunctionTypes:函数参数类型严格检查strictBindCallApply:bind、call、apply的参数类型检查noImplicitThis:不允许隐式this类型alwaysStrict:在文件顶部添加"use strict"strictPropertyInitialization:类属性必须在构造函数中初始化
2.5 any、unknown 和 never
2.5.1 any 类型
any 是 TypeScript 的"逃生舱口"——它关闭所有类型检查:
let value: any = "hello";
value = 42; // OK
value = true; // OK
value.foo.bar(); // OK(但运行时可能报错)应该尽量避免使用 any,它是类型安全的敌人。
2.5.2 unknown 类型
unknown 是 any 的类型安全替代——你必须先检查类型才能使用:
let value: unknown = "hello";
// value.toUpperCase(); // 错误!unknown 不能直接使用
if (typeof value === "string") {
value.toUpperCase(); // OK! TypeScript 知道这里是 string
}
// 类型守卫
function isString(val: unknown): val is string {
return typeof val === "string";
}
if (isString(value)) {
value.toUpperCase(); // OK!
}2.5.3 never 类型
never 表示"永远不会发生"——它用于穷举检查和不可能到达的代码:
function throwError(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {}
}
// 穷举检查
type Shape = "circle" | "square" | "triangle";
function getArea(shape: Shape): number {
switch (shape) {
case "circle": return Math.PI * 1;
case "square": return 1 * 1;
case "triangle": return 0.5 * 1 * 1;
default:
const _exhaustive: never = shape;
return _exhaustive;
}
}三、实战案例
3.1 案例一:从 JavaScript 迁移到 TypeScript
// JavaScript 原始代码
function fetchUserData(userId) {
return fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => ({
name: data.name,
email: data.email,
joinedAt: new Date(data.joined_at)
}));
}
// TypeScript 迁移后
interface User {
name: string;
email: string;
joinedAt: Date;
}
interface UserResponse {
name: string;
email: string;
joined_at: string;
}
async function fetchUserData(userId: string): Promise<User> {
const response = await fetch(`/api/users/${userId}`);
const data: UserResponse = await response.json();
return {
name: data.name,
email: data.email,
joinedAt: new Date(data.joined_at)
};
}3.2 案例二:类型安全的 DOM 操作
// 类型安全的 DOM 查询
function getElement<T extends HTMLElement>(
selector: string,
type: new () => T
): T {
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Element not found: ${selector}`);
}
if (!(element instanceof type)) {
throw new Error(`Element is not of type ${type.name}`);
}
return element;
}
// 使用
const input = getElement("input#name", HTMLInputElement);
input.value = "Alice"; // 类型安全!
const canvas = getElement("canvas#game", HTMLCanvasElement);
const ctx = canvas.getContext("2d"); // 类型安全!3.3 案例三:类型安全的配置验证
interface AppConfig {
port: number;
host: string;
database: {
url: string;
maxConnections: number;
};
logging: {
level: "debug" | "info" | "warn" | "error";
file: string;
};
}
function validateConfig(raw: unknown): AppConfig {
if (typeof raw !== "object" || raw === null) {
throw new Error("Config must be an object");
}
const config = raw as Record<string, unknown>;
const port = config.port;
if (typeof port !== "number" || port < 1 || port > 65535) {
throw new Error("Invalid port");
}
const host = config.host;
if (typeof host !== "string" || host.length === 0) {
throw new Error("Invalid host");
}
return {
port,
host,
database: config.database as AppConfig["database"],
logging: config.logging as AppConfig["logging"]
};
}四、本章小结
第1章建立了 TypeScript 的基础认知:
渐进式类型化:TypeScript 允许你逐步引入类型,不需要一次性重写所有代码。
结构化类型系统:类型兼容性由结构决定,而非名称。
类型推断:编译器能从上下文推断大多数类型,减少冗余的类型注解。
strict 模式:开启严格的类型检查,避免隐式 any 和空值问题。
any vs unknown vs never:any 关闭类型检查,unknown 要求类型守卫,never 表示不可能。
五、思考与练习
思考题
TypeScript 的结构化类型系统相比 Java 的名义类型系统有什么优缺点?
为什么推荐开启 strict: true?noImplicitAny 和 strictNullChecks 分别解决了什么问题?
unknown 相比 any 的优势是什么?在什么场景下应该使用 unknown?
TypeScript 的类型信息在编译后会消失吗?这对运行时有什么影响?
编程练习
将一个 JavaScript 文件(如一个简单的工具函数库)迁移到 TypeScript,添加适当的类型注解。
编写一个类型安全的函数,接收一个未知类型的参数,通过类型守卫判断其类型并执行相应操作。
创建一个 tsconfig.json 配置,开启所有 strict 选项,并解释每个选项的作用。