1Programming TypeScript

TypeScript入门

类型基础

类型注解类型推断基本类型类型别名

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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 文件开始,逐步添加类型注解:

typescript
// 阶段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)。这意味着类型的兼容性由结构决定,而不是由名称决定:

typescript
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 原始类型

typescript
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 数组与元组

typescript
// 数组
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 对象类型

typescript
// 内联对象类型
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 的类型推断非常强大,大多数时候不需要显式标注类型:

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 自动推断为 number

2.3.1 最佳公共类型

typescript
// 编译器会选择最宽泛的公共类型
let arr = [1, "hello", true];    // (string | number | boolean)[]

// 空数组推断为 any[]
let empty = [];    // any[]
let emptyTyped: string[] = [];    // string[]

2.4 配置 TypeScript

2.4.1 tsconfig.json

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 的"逃生舱口"——它关闭所有类型检查:

typescript
let value: any = "hello";
value = 42;        // OK
value = true;      // OK
value.foo.bar();   // OK(但运行时可能报错)

应该尽量避免使用 any,它是类型安全的敌人。

2.5.2 unknown 类型

unknown 是 any 的类型安全替代——你必须先检查类型才能使用:

typescript
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 表示"永远不会发生"——它用于穷举检查和不可能到达的代码:

typescript
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

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 操作

typescript
// 类型安全的 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 案例三:类型安全的配置验证

typescript
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 选项,并解释每个选项的作用。


1/7Type
0%