ts随笔:面向对象与高级类型
本篇主要聚焦在类、模块、高级类型以及在常见前端框架中的实践,同时结合生态中新出现的一些特性,如何自然地用上这些新能力。
原文地址
类(Class)
类是面向对象编程的基础,用于创建具有属性(数据成员)和方法(成员函数)的对象的蓝图。TypeScript 中的类支持 继承、封装、多态 等面向对象特性。
基本语法
typescript
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
const person = new Person("Alice", 30);
person.greet();
在较新的 TypeScript 版本中,你也可以结合 ECMAScript 的 私有字段 语法(以 # 开头),在保持类型安全的同时实现更彻底的封装:
typescript
class Counter {
#value = 0;
increment() {
this.#value++;
}
get value(): number {
return this.#value;
}
}
继承
typescript
class Student extends Person {
studentId: string;
constructor(name: string, age: number, studentId: string) {
super(name, age);
this.studentId = studentId;
}
study() {
console.log(`${this.name} is studying.`);
}
}
const student = new Student("Bob", 20, "S123");
student.greet();
student.study();
借助 TypeScript 的严格类型系统,继承关系中的属性和方法都会得到完整的类型检查支持,在重写方法时也能获得参数和返回值的约束。
模块(Module)
模块是用于组织代码的容器,它允许你将相关联的类、接口、函数等封装在一个单独的文件中,并可以控制它们的可见性(导出/导入)。模块有助于避免命名冲突和促进代码的复用。
导入与导出
typescript
// moduleA.ts
export class MyClass {
// ...
}
// 在其他文件中使用导出的元素
import { MyClass } from "./moduleA";
const myInstance = new MyClass();
默认导出 与 命名导出 可以混合使用,但在一个模块中只能有一个默认导出;命名导出则可以有多个。
命名空间与模块的异同
在早期版本的 TypeScript 中,命名空间(Namespace)是另一种组织代码的方式,它类似于 C# 或 Java 中的包,提供了一种分层次的方式来组织代码。虽然模块现在是推荐的做法,但命名空间仍然可用,特别是在需要合并多个文件定义的命名空间时。
面向未来的模块特性:JSON 模块 与 import defer
从 ES2025 开始,JSON 模块 等特性有望在主流环境中稳定可用,你可以直接以模块的方式导入 JSON 文件,并配合 TypeScript 的类型系统进行约束:
typescript
// config.json
// {
// "apiBaseUrl": "https://api.example.com",
// "featureFlags": {
// "newUI": true
// }
// }
interface FeatureFlags {
newUI: boolean;
}
interface AppConfig {
apiBaseUrl: string;
featureFlags: FeatureFlags;
}
// 在支持 JSON 模块的环境下
import configJson from "./config.json" with { type: "json" };
const config = configJson as AppConfig;
在 ES2026 及之后,import defer 等语法提案逐步成熟时,可以在保持语义清晰的前提下延迟加载非关键模块,而 TypeScript 依然会对导入的符号进行完整的类型检查:
typescript
// 伪代码示意:具体语法以最终标准为准
// import defer "./heavy-analytics.js";
// TypeScript 关注的是导出的类型本身,只要声明文件同步更新,
// 即使底层加载时机发生变化,类型系统仍然保持稳定。
和上一篇中提到的声明文件一样,这些新的模块特性最终都会通过 .d.ts 的方式落地到 TypeScript 生态中。
高级类型探索
泛型 Generics
泛型(Generics)是 TypeScript 中一个强大的特性,它允许你在定义函数、接口或类的时候不预先指定具体的类型,而是将类型作为参数传递。
基本概念
泛型的核心在于使用类型变量(通常用大写字母表示,如 T、U 等)来代表一些未知的类型。当使用这个组件时,你再指定这些类型变量的具体类型。
泛型函数
typescript
function identity<T>(arg: T): T {
return arg;
}
let output = identity<string>("hello");
console.log(output);
let numberOutput = identity<number>(123);
console.log(numberOutput);
泛型接口
typescript
interface Pair<T> {
first: T;
second: T;
}
let pairStr: Pair<string> = { first: "hello", second: "world" };
let pairNum: Pair<number> = { first: 1, second: 2 };
泛型类
typescript
class Box<T> {
private containedValue: T;
set(value: T) {
this.containedValue = value;
}
get(): T {
return this.containedValue;
}
}
let boxStr = new Box<string>();
boxStr.set("hello");
console.log(boxStr.get());
let boxNum = new Box<number>();
boxNum.set(123);
console.log(boxNum.get());
泛型约束
有时候,你可能需要限制可以作为类型参数的具体类型,这时候可以使用泛型约束。泛型约束通过接口来定义,要求传入的类型必须满足该接口定义的条件。
typescript
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity({ length: 10, value: "test" });
// loggingIdentity(123); // 错误:number 没有 length 属性
联合类型 Union Types
联合类型 允许一个变量可能是多种类型之一。例如,你可以定义一个变量既可能是字符串也可能是数字:
typescript
let myValue: string | number;
myValue = "Hello";
myValue = 42;
类型守卫 Type Guards
当你在操作联合类型的变量时,TypeScript 可能无法确定变量的具体类型,这会影响到你能够调用的方法或访问的属性。类型守卫 就是用来缩小类型范围,确保在运行时变量属于某种特定类型。
typeof 类型守卫
typescript
if (typeof myValue === "string") {
console.log(myValue.toUpperCase());
} else {
console.log(myValue.toFixed(2));
}
instanceof 类型守卫
typescript
class Animal {}
class Dog extends Animal {
bark() {
console.log("Woof!");
}
}
function isDog(animal: Animal): animal is Dog {
return animal instanceof Dog;
}
let pet = new Dog();
if (isDog(pet)) {
pet.bark();
}
in 操作符
typescript
interface Cat {
meow: () => void;
}
function makeSound(animal: Animal | Cat) {
if ("meow" in animal) {
animal.meow();
} else {
console.log(animal.toString());
}
}
Iterator Helpers 与 Set 扩展下的类型推断
在 ES2025、ES2026 相关提案中,Iterator Helpers 和 Set 扩展 是非常值得关注的一类特性:它们让各种可迭代对象(包括数组、Set、Map 的键值迭代器等)拥有类似链式操作的能力。
当对应的类型定义进入 TypeScript 之后,可以配合泛型和类型守卫写出既简洁又安全的代码。例如,以 Set 扩展为例:
typescript
// 假设运行时与 TypeScript lib 均已支持 Set 的扩展方法
const ids = new Set([1, 2, 3, 4, 5]);
// filter 返回的仍然是 Set<number>,类型信息由泛型推断而来
// const evenIds = ids.filter((id) => id % 2 === 0);
// map 等其他 Iterator Helpers 也同理可以得到明确的类型
// const idStrings = ids.map((id) => `id-${id}`);
虽然上面的代码在当前某些环境中还处于"提案阶段",但可以预期的是,未来在 TypeScript 中使用这些 API 时,你同样能获得完整的泛型推断和类型守卫支持。
日期时间与本地化:Temporal 与 Intl.Locale
时间与本地化一直是前端开发中的老大难问题。Temporal 和 Intl.Locale 等提案正是为了解决 Date 语义不清、Intl 配置复杂等问题。
在 Temporal 定稿并进入主流运行时时,你可以在 TypeScript 中这样书写代码:
typescript
// 假设 lib 已经包含 Temporal 与最新的 Intl 声明
// const now: Temporal.ZonedDateTime = Temporal.Now.zonedDateTimeISO();
// const locale = new Intl.Locale("zh-CN", { calendar: "gregory" });
// console.log(now.toLocaleString(locale.toString()));
这些 API 本身是 JavaScript 语言层面的特性,但它们的类型声明会第一时间进入 TypeScript 官方声明文件,从而让我们在使用它们时也能享受完整的类型推断、自动补全和错误检查。
ts 在 React 中的使用
新项目使用 create-react-app 接入
bash
npx create-react-app my-app --template typescript
React 老项目接入
首先安装 @types/react 和 @types/react-dom 这些 React 的类型定义文件:
bash
npm install --save-dev @types/react @types/react-dom
然后将 .js 文件逐步转换为 .tsx(TypeScript 支持 JSX 的文件扩展名)并添加类型注释。
React 代码编写
tsx
import React, { useState } from "react";
interface Props {
name: string;
}
const Hello: React.FC<Props> = ({ name }) => {
const [message, setMessage] = useState<string>("Hello");
return (
<div>
<h1>{`${message}, ${name}!`}</h1>
<button onClick={() => setMessage("Welcome")}>Change Message</button>
</div>
);
};
export default Hello;
在较新的 TypeScript 与 React 生态中,配合前面提到的 JSON 模块、Iterator Helpers、Temporal 等能力,你可以更放心地在组件中使用这些新特性------只要升级依赖并确保声明文件同步更新,编辑器就会用类型系统帮你"兜住"大部分错误。
ts 在 Vue 3 中的使用
新项目使用 Vue CLI 接入
bash
vue create my-vue3-project --preset typescript
Vue 老项目接入
bash
vue add typescript
Vue 代码编写
html
<script lang="ts">
import { defineComponent, ref, reactive } from "vue";
interface Props {
msg: string;
}
export default defineComponent({
props: {
msg: String,
},
setup(props: Props) {
const count = ref(0);
const state = reactive({ status: "active" });
// 在这里同样可以安心地使用前文提到的高级类型、
// Iterator Helpers 或 Temporal 等能力,TypeScript
// 会在编译阶段帮你把控类型安全。
return {
count,
state,
};
},
});
</script>
无论是 React 还是 Vue,TypeScript 都会继续扮演"粘合剂"的角色:只要按需升级依赖、合理配置 tsconfig,就能够在习惯的写法下自然享受到这些新特性带来的收益。