第2章 类型系统入门
本章定位:理解TypeScript静态类型系统的核心概念。对比Python的type hint,掌握如何在TypeScript中声明和使用类型,以及类型推断的工作原理。
2.1 静态类型 vs 动态类型
Section titled “2.1 静态类型 vs 动态类型”静态类型:变量类型在编译时确定,编译时进行类型检查。代表语言:C++、Java、TypeScript。
动态类型:变量类型在运行时确定,类型可以随时改变。代表语言:Python、JavaScript、Ruby。
TypeScript的态度
Section titled “TypeScript的态度”TypeScript采用”可选静态类型”的设计哲学:
- 可以显式声明类型
- 可以让TypeScript自动推断类型
- 完全兼容JavaScript的动态行为
这意味着你可以逐步将JavaScript代码迁移到TypeScript,而不是要求一开始就完整标注。
Python对比
Section titled “Python对比”Python是动态类型语言,type hint只是给程序员看的提示,不强制执行:
def process(x: int) -> str: return str(x)
# 完全可以传入任何类型,Python解释器不报错result = process("hello") # 运行时报错:TypeErrorTypeScript在编译时就能发现这个错误:
function process(x: number): string { return String(x);}
// 编译错误:Argument of type 'string' is not assignable to parameter of type 'number'const result = process("hello");类型检查的价值
Section titled “类型检查的价值”// 场景:处理用户输入function formatUsername(name: string): string { return name.trim().toLowerCase();}
// 类型检查确保传入的是字符串formatUsername(123); // 编译错误!formatUsername(" Alice "); // 编译通过,输出 "alice"Python等效代码(无编译时保护):
def format_username(name: str) -> str: return name.strip().lower()
# Python解释器允许,但运行时报TypeErrorformat_username(123) # TypeError: 'int' object has no attribute 'strip'TypeScript支持渐进式迁移:
// Step 1: 纯JavaScript(any类型)function add(a, b) { return a + b;}
// Step 2: 部分添加类型function add(a: number, b: number) { return a + b;}
// Step 3: 完整类型const add: (a: number, b: number) => number = (a, b) => { return a + b;};2.2 基础类型:string、number、boolean
Section titled “2.2 基础类型:string、number、boolean”string(字符串)
Section titled “string(字符串)”TypeScript:
const name: string = "Alice";const greeting: string = 'Hello';const template: string = `Hi, ${name}!`;
name = 123; // 错误!类型不匹配Python对比:
name: str = "Alice"greeting: str = 'Hello'template: str = f"Hi, {name}!"字符串常用操作:
const str: string = "Hello, TypeScript";
// 属性访问(只读)str[0]; // "H"str.charAt(0); // "H"
// 查找str.indexOf("Type"); // 7str.includes("abc"); // falsestr.startsWith("Hello"); // truestr.endsWith("Script"); // true
// 修改(返回新字符串,原字符串不变)str.toUpperCase(); // "HELLO, TYPESCRIPT"str.toLowerCase(); // "hello, typescript"str.trim(); // "Hello, TypeScript"str.trimStart(); // "Hello, TypeScript "(去尾部空格)str.trimEnd(); // " Hello, TypeScript"(去首部空格)
// 切片/分割str.slice(0, 5); // "Hello"str.substring(0, 5); // "Hello"str.split(", "); // ["Hello", "TypeScript"]str.replace("TypeScript", "World"); // "Hello, World"str.concat("!"); // "Hello, TypeScript!"
// 重复str.repeat(2); // "Hello, TypeScriptHello, TypeScript"str.padStart(20, "-"); // "---Hello, TypeScript"str.padEnd(20, "-"); // "Hello, TypeScript---"
// 长度str.length; // 18Python对比:
s = "Hello, TypeScript"s[0] # "H"s.index("Type") # 7s.upper() # "HELLO, TYPESCRIPT"s.split(", ") # ["Hello", "TypeScript"]s.replace("TypeScript", "World") # "Hello, World"len(s) # 18number(数字)
Section titled “number(数字)”TypeScript:
const integer: number = 42;const float: number = 3.14;const negative: number = -10;const hex: number = 0xFF; // 255const binary: number = 0b1010; // 10const octal: number = 0o777; // 511const bigInt: bigint = 1000000000000000000n; // BigIntPython对比:
integer: int = 42float_num: float = 3.14negative: int = -10hex_num: int = 0xFF # 255binary_num: int = 0b1010 # 10octal_num: int = 0o777 # 511big_int: int = 10**18 # Python整数支持任意精度数字常用操作:
// 算术运算Math.abs(-5); // 5Math.floor(3.7); // 3Math.ceil(3.2); // 4Math.round(3.5); // 4Math.trunc(3.9); // 3Math.sqrt(16); // 4Math.pow(2, 10); // 1024Math.max(1, 5, 3); // 5Math.min(1, 5, 3); // 1
// 常量Math.PI; // 3.141592653589793Math.E; // 2.718281828459045
// 特殊值Number.MAX_VALUE; // 1.7976931348623157e+308Number.MIN_VALUE; // 5e-324Number.NaN; // NaNNumber.POSITIVE_INFINITY; // InfinityNumber.NEGATIVE_INFINITY; // -Infinity
// 字符串转数字parseInt("42"); // 42parseInt("42.5"); // 42parseFloat("3.14"); // 3.14Number("42"); // 42Number("42.5"); // 42.5Number("hello"); // NaN
// 数字转字符串String(42); // "42"(42).toString(); // "42"(42).toFixed(2); // "42.00"(3.14159).toPrecision(4); // "3.142"(255).toString(16); // "ff"Python对比:
import math
abs(-5) # 5math.floor(3.7) # 3math.ceil(3.2) # 4round(3.5) # 4math.sqrt(16) # 4.0max(1, 5, 3) # 5min(1, 5, 3) # 1
# 常量math.pi # 3.141592653589793math.e # 2.718281828459045
# 字符串转数字int("42") # 42int("42.5") # ValueError!float("3.14") # 3.14int("hello") # ValueError!
# 数字转字符串str(42) # "42"format(42, ".2f") # "42.00"format(3.14159, ".4f") # "3.1416"hex(255) # '0xff'浮点数精度问题:
0.1 + 0.2; // 0.30000000000000004(经典问题)(0.1 + 0.2 === 0.3); // false!
// 解决方案:使用整数运算或toFixedMath.abs(0.1 + 0.2 - 0.3) < Number.EPSILON; // trueparseFloat((0.1 + 0.2).toFixed(2)); // 0.3Python中同样的问题:
0.1 + 0.2 # 0.30000000000000004boolean(布尔)
Section titled “boolean(布尔)”TypeScript:
const isActive: boolean = true;const isLoggedIn: boolean = false;
// 布尔运算true && false; // falsetrue || false; // true!true; // false
// 短路求值isActive && console.log("Active!");false && someFunction(); // 不执行someFunctiontrue || someFunction(); // 不执行someFunctionPython对比:
is_active: bool = Trueis_logged_in: bool = False
# 布尔运算True and False # FalseTrue or False # Truenot True # False
# 短路求值if is_active: print("Active!")Truthy与Falsy:
TypeScript/JavaScript中,以下值在布尔上下文中被视为false(falsy):
false0、-0、0n""(空字符串)nullundefinedNaN
其他所有值都是true(truthy)。
if ("") { console.log("empty string is truthy");} // 不会执行
if (0) { console.log("0 is truthy");} // 不会执行
if ("hello") { console.log("non-empty string is truthy");} // 会执行
if ([]) { console.log("empty array is truthy");} // 会执行!数组即使是空的也是truthyPython对比:
# Python中,以下值是falsyFalseNone00.0[]{}""set()
# 其他都是truthyif "hello": print("truthy") # 执行if []: print("empty list is falsy") # 不执行基础类型检查
Section titled “基础类型检查”function printInfo(value: string | number | boolean): void { if (typeof value === "string") { console.log(`String: ${value.toUpperCase()}`); } else if (typeof value === "number") { console.log(`Number: ${value.toFixed(2)}`); } else { console.log(`Boolean: ${value}`); }}
printInfo("hello"); // String: HELLOprintInfo(42); // Number: 42.00printInfo(true); // Boolean: true2.3 类型推断:你不需要处处标注类型
Section titled “2.3 类型推断:你不需要处处标注类型”TypeScript的编译器能够根据变量的赋值上下文自动推断变量类型。这称为”类型推断”(Type Inference)。
基本推断规则
Section titled “基本推断规则”// 显式标注let name: string = "Alice";
// 类型推断(等价)let name2 = "Alice"; // TypeScript推断为stringname2 = 123; // 错误!不能将number赋值给string
// 函数返回类型推断function add(a: number, b: number) { return a + b; // 推断返回类型为number}const result = add(1, 2); // result是number
// 数组类型推断const numbers = [1, 2, 3]; // number[]const names = ["Alice", "Bob"]; // string[]const mixed = [1, "hello"]; // (number | string)[]上下文类型推断
Section titled “上下文类型推断”// 箭头函数的参数类型可以从上下文推断const double: (x: number) => number = x => x * 2;// 等价于const double2 = (x: number): number => x * 2;
// 对象字面量的类型推断const person = { name: "Alice", age: 30};person.name = "Bob"; // OKperson.age = "thirty"; // 错误!age是number类型推断的局限
Section titled “类型推断的局限”// 初始值为any时,无法推断具体类型let value = null; // anyvalue = 123; // anyvalue = "hello"; // any
// 声明但未初始化,无法推断let flag; // anyflag = true; // OKflag = "yes"; // OK
// 最佳实践:声明时初始化,或显式标注类型let flag: boolean; // 显式标注flag = true; // OKPython对比:
# Python不需要类型标注,完全动态value = Nonevalue = 123value = "hello"
# Python不做任何类型推断检查flag = Trueflag = "yes" # 完全OK,Python不报错-
让TypeScript推断简单变量类型
// 推荐:类型推断const name = "Alice";const count = 10;const isValid = true;// 不推荐:显式标注简单类型const name: string = "Alice"; -
显式标注复杂类型
// 推荐:标注函数参数和返回值function process(data: string[]): number {return data.reduce((acc, s) => acc + s.length, 0);}// 推荐:标注复杂对象interface User {name: string;age: number;}const user: User = { name: "Alice", age: 30 }; -
使用类型断言处理已知类型
// 当TypeScript无法推断时,可以手动断言const input = document.getElementById("myInput") as HTMLInputElement;const value = input.value; // TypeScript知道是string
2.4 类型注解:显式声明类型
Section titled “2.4 类型注解:显式声明类型”变量类型注解
Section titled “变量类型注解”// 基础类型let name: string = "Alice";let age: number = 30;let isStudent: boolean = false;
// 数组let scores: number[] = [95, 87, 92];let names: Array<string> = ["Alice", "Bob"]; // 泛型语法
// 元组let coordinate: [number, number] = [10, 20];
// 对象let user: { name: string; age: number } = { name: "Alice", age: 30};函数类型注解
Section titled “函数类型注解”// 函数声明function add(a: number, b: number): number { return a + b;}
// 箭头函数const multiply = (x: number, y: number): number => x * y;
// 可选参数function greet(name: string, greeting?: string): string { return greeting ? `${greeting}, ${name}!` : `Hello, ${name}!`;}
// 默认参数function createUser(name: string, age: number = 18): User { return { name, age };}
// 剩余参数function sum(...numbers: number[]): number { return numbers.reduce((acc, n) => acc + n, 0);}
// 返回void(不返回值的函数)function logMessage(message: string): void { console.log(message);}
// 返回never(永不返回的函数)function throwError(message: string): never { throw new Error(message);}
// 函数类型let myAdd: (x: number, y: number) => number;myAdd = (a, b) => a + b;myAdd = function(a, b) { return a + b; };类型别名与接口
Section titled “类型别名与接口”// 类型别名type UserName = string;type UserAge = number;
type Point = { x: number; y: number;};
// 接口interface User { name: string; age: number; email?: string; // 可选属性}
// 联合类型type Status = "pending" | "approved" | "rejected";type ID = string | number;
// 交叉类型type Employee = User & { employeeId: string; department: string;};泛型类型注解
Section titled “泛型类型注解”// 泛型函数function identity<T>(arg: T): T { return arg;}
const num = identity<number>(42);const str = identity<string>("hello");
// 泛型接口interface Container<T> { value: T; getValue(): T;}
// 泛型类class Box<T> { private content: T;
constructor(value: T) { this.content = value; }
get(): T { return this.content; }}
const numberBox = new Box<number>(42);const stringBox = new Box<string>("hello");模板字面量类型
Section titled “模板字面量类型”type World = "world";type Greeting = `hello, ${World}`; // "hello, world"
type Email = `${string}@${string}.${string}`;type HttpsUrl = `https://${string}`;
function createUrl(path: string): `/${string}` { return `/${path}`;}2.5 Python的type hint与TypeScript的类型系统对比
Section titled “2.5 Python的type hint与TypeScript的类型系统对比”基础语法对比
Section titled “基础语法对比”Python type hint:
from typing import List, Optional, Callable
def process( name: str, count: int, items: List[str], callback: Optional[Callable[[int], None]] = None) -> List[int]: results = [len(item) * count for item in items] if callback: callback(sum(results)) return resultsTypeScript类型注解:
function process( name: string, count: number, items: string[], callback?: (sum: number) => void): number[] { const results = items.map(item => item.length * count); if (callback) { callback(results.reduce((a, b) => a + b, 0)); } return results;}类型种类对比
Section titled “类型种类对比”| Python | TypeScript | 说明 |
|---|---|---|
int | number | 整数和浮点数统一 |
float | number | 无区分 |
str | string | 字符串 |
bool | boolean | 布尔值 |
list[int] | number[] | 数组 |
dict[str, int] | Record<string, number> | 字典 |
tuple[int, str] | [number, string] | 元组 |
Optional[int] | number | undefined | 可选值 |
Union[int, str] | number | string | 联合类型 |
Callable[[int], str] | (x: number) => string | 函数类型 |
None | null / undefined | 空值 |
运行时 vs 编译时
Section titled “运行时 vs 编译时”Python type hint不强制执行:
def double(x: int) -> int: return x * 2
double("hello") # 运行时报错:TypeErrordouble([1, 2]) # 运行时报错:TypeErrorTypeScript编译时检查:
function double(x: number): number { return x * 2;}
double("hello"); // 编译错误!double([1, 2]); // 编译错误!类型别名的Python实现
Section titled “类型别名的Python实现”Python:
from typing import TypeAlias
UserId: TypeAlias = int | strPoint: TypeAlias = tuple[float, float]
# 或使用TypeAlias注解(旧写法)from typing import Union, Tuple
UserId = Union[int, str]Point = Tuple[float, float]TypeScript:
type UserId = number | string;type Point = [number, number];Python泛型(3.9+):
from typing import TypeVar
T = TypeVar("T")
def identity(x: T) -> T: return x
# 约束泛型Comparable = TypeVar("Comparable", bound=int | float)
def maximum(a: Comparable, b: Comparable) -> Comparable: return a if a > b else bTypeScript泛型:
function identity<T>(x: T): T { return x;}
// 约束泛型function maximum<T extends number | string>(a: T, b: T): T { return a > b ? a : b;}
// 泛型约束interface HasLength { length: number;}
function logLength<T extends HasLength>(arg: T): number { return arg.length;}类型检查工具
Section titled “类型检查工具”Python:需要额外工具如mypy、pyright
pip install mypymypy my_script.pyTypeScript:内置编译器检查
tsc --noEmit my_script.ts2.6 any与unknown:类型安全的双重保障
Section titled “2.6 any与unknown:类型安全的双重保障”any:任意类型
Section titled “any:任意类型”any类型代表”任意类型”,使用它可以绕过所有类型检查。
let value: any = 123;value = "hello";value = [1, 2, 3];value(); // 完全合法,TypeScript不报错value.name; // 完全合法何时使用any:
- 逐步迁移JavaScript代码到TypeScript
- 无法确定变量类型时
- 第三方库返回复杂类型
为何慎用any:
- 丧失TypeScript的类型安全优势
- 错误只能在运行时发现
- IDE无法提供准确的代码补全
unknown:安全的任意类型
Section titled “unknown:安全的任意类型”unknown是any的类型安全版本。使用前必须先进行类型检查。
let value: unknown = 123;value = "hello";value = [1, 2, 3];
// 使用前必须类型检查if (typeof value === "string") { console.log(value.toUpperCase()); // OK}
// 错误示例value(); // 编译错误!不能调用unknown类型value.name; // 编译错误!不能访问unknown属性实际应用对比
Section titled “实际应用对比”// 使用any(不推荐)function parseJSON(json: string): any { return JSON.parse(json);}
const data = parseJSON('{"name": "Alice"}');console.log(data.name); // 编译通过,但运行可能出错// 如果JSON是'["a", "b"]',data.name是undefined// 使用unknown(推荐)function parseJSON(json: string): unknown { return JSON.parse(json);}
const data = parseJSON('{"name": "Alice"}');
// 安全地检查和使用if (typeof data === "object" && data !== null && !Array.isArray(data)) { if ("name" in data && typeof data.name === "string") { console.log(data.name); // 安全的 }}unknown的多种类型检查方式
Section titled “unknown的多种类型检查方式”function process(value: unknown): void { // 1. typeof检查基础类型 if (typeof value === "string") { console.log(value.toUpperCase()); } // 2. instanceof检查类实例 else if (value instanceof Date) { console.log(value.toISOString()); } // 3. in检查属性存在 else if (typeof value === "object" && value !== null && "name" in value) { console.log((value as { name: string }).name); } // 4. Array.isArray检查数组 else if (Array.isArray(value)) { console.log(`Array with ${value.length} elements`); } // 5. 类型谓词函数 else if (isUser(value)) { console.log(value.name); }}
// 自定义类型谓词function isUser(obj: unknown): obj is { name: string; age: number } { return ( typeof obj === "object" && obj !== null && "name" in obj && "age" in obj && typeof (obj as any).name === "string" && typeof (obj as any).age === "number" );}unknown vs any 对比
Section titled “unknown vs any 对比”| 特性 | any | unknown |
|---|---|---|
| 类型安全 | 无 | 有 |
| 赋值给其他类型 | 都可以 | 必须类型检查后才能赋值 |
| 调用方法 | 都可以 | 不可以 |
| 访问属性 | 都可以 | 不可以 |
| 推荐使用场景 | 快速原型、迁移JS代码 | 处理外部数据、API响应 |
2.7 类型宽恕与类型严格模式
Section titled “2.7 类型宽恕与类型严格模式”tsconfig.json 中的严格模式
Section titled “tsconfig.json 中的严格模式”{ "compilerOptions": { "strict": true }}strict: true 启用所有严格类型检查选项,是最佳实践。
各个严格检查选项详解
Section titled “各个严格检查选项详解”noImplicitAny
Section titled “noImplicitAny”不允许隐式any类型。
// 错误:参数没有显式类型,且无法推断function process(x) { // Error: Parameter 'x' implicitly has an 'any' type return x;}
// 正确:显式标注或让TypeScript推断function process(x: number) { return x;}Python无对应概念,因为Python中所有参数都可以是任意类型。
strictNullChecks
Section titled “strictNullChecks”严格检查null和undefined。
// 错误:可能访问null/undefined的属性function getLength(str: string | null): number { return str.length; // Error: 'str' is possibly 'null'}
// 正确:先检查nullfunction getLength(str: string | null): number { if (str === null) { return 0; } return str.length;}
// 或者使用可选链function getLength(str: string | null): number { return str?.length ?? 0;}Python对比:
def get_length(s: str | None) -> int: if s is None: return 0 return len(s)noImplicitReturns
Section titled “noImplicitReturns”函数必须对所有代码路径有返回值。
// 错误:不是所有路径都返回值function getGrade(score: number): string { if (score >= 90) { return "A"; } else if (score >= 80) { return "B"; } // Error: Not all code paths return a value}
// 正确:所有路径都有返回值function getGrade(score: number): string { if (score >= 90) { return "A"; } else if (score >= 80) { return "B"; } else if (score >= 70) { return "C"; } else { return "F"; }}strictPropertyInitialization
Section titled “strictPropertyInitialization”类的属性必须在构造函数中初始化。
class User { name: string; // Error: Property 'name' has no initializer age: number;
constructor(age: number) { this.age = age; // Error: Property 'name' must be initialized }}
// 正确:初始化所有属性class User { name: string; age: number;
constructor(name: string, age: number) { this.name = name; this.age = age; }}
// 或者使用确定赋值断言class User { name!: string; // 告诉TS我会赋值 age: number;
constructor(age: number) { this.age = age; this.name = "Unknown"; }}
// 或者可选属性class User { name?: string; age: number;}Python无此要求,因为Python的属性可以动态添加。
非严格模式下的宽松行为
Section titled “非严格模式下的宽松行为”关闭严格模式后,TypeScript的行为更接近JavaScript:
// strictNullChecks: false 时function getLength(str: string | null): number { return str.length; // OK! 可能运行时错误}
// noImplicitAny: false 时function process(x) { // OK! x是隐式any return x * 2;}
// strictPropertyInitialization: false 时class User { name: string; // OK! 不要求初始化}逐步迁移策略
Section titled “逐步迁移策略”如果项目已有大量代码,可以逐步启用严格模式:
{ "compilerOptions": { "strict": false, "noImplicitAny": true, "strictNullChecks": true }}然后逐步添加其他选项:
{ "compilerOptions": { "strict": false, "noImplicitAny": true, "strictNullChecks": true, "strictPropertyInitialization": true }}直到完全启用strict: true。
2.8 本章小结
Section titled “2.8 本章小结”本章深入学习了TypeScript的类型系统:
-
静态类型 vs 动态类型:
- TypeScript在编译时检查类型,Python主要靠运行时
- TypeScript提供可选的静态类型
-
基础类型:
string、number、boolean是三种基础类型- 与Python的
str、int/float、bool对应 - TypeScript的数字不区分整数和浮点
-
类型推断:
- TypeScript能自动推断简单变量类型
- 函数返回值、数组元素类型都能推断
- 复杂类型建议显式标注
-
类型注解:
- 变量、函数参数、函数返回值都可以显式标注
- 接口和类型别名用于复杂类型
- 泛型提供灵活的代码复用
-
Python type hint对比:
- Python的type hint仅供阅读,不强制
- TypeScript的类型检查是编译时的
- 两者语法和概念有很多相似之处
-
any vs unknown:
any绕过所有类型检查,慎用unknown要求使用前进行类型检查,更安全- 优先使用
unknown而不是any
-
严格模式:
strict: true启用所有严格检查- 可逐步启用各个选项
- 建议在项目中完全启用严格模式
练习2.1:基础类型标注
Section titled “练习2.1:基础类型标注”创建以下变量并标注类型:
- 一个字符串变量存储用户名
- 一个数字变量存储年龄
- 一个布尔变量存储是否登录
- 一个数组存储多个分数
练习2.2:函数类型标注
Section titled “练习2.2:函数类型标注”编写以下函数,带完整类型标注:
formatDate(date: Date): string- 返回格式化的日期字符串filterByStatus(items: {name: string, status: string}[], status: string): {name: string, status: string}[]- 按状态过滤数组
练习2.3:类型推断实验
Section titled “练习2.3:类型推断实验”尝试以下实验:
- 声明一个数组
const arr = [1, 2, 3],查看TypeScript推断的类型 - 声明一个函数
const add = (a, b) => a + b,查看参数类型 - 修改
arr为arr.push("hello"),观察编译错误
练习2.4:any vs unknown
Section titled “练习2.4:any vs unknown”编写一个函数safeParse(json: string): unknown,解析JSON并安全返回。然后在另一个函数中安全地使用返回结果。
练习2.5:严格模式探索
Section titled “练习2.5:严格模式探索”- 在项目中创建一个
tsconfig.strict.json,启用所有严格选项 - 尝试编译现有代码,观察有哪些错误
- 尝试修复这些错误
下一章我们将学习TypeScript的控制流与函数,掌握函数的各种声明方式、参数类型和返回类型,以及与Python函数的异同。