TypeScript入门完全指南:从JavaScript到类型安全

TypeScript入门完全指南封面 - 从JavaScript到类型安全的前端开发教程

引言

说实话,我第一次听说TypeScript的时候是拒绝的。”JavaScript不是挺好的吗?为什么还要学一个新的语言?”这是我当时的真实想法。

但当我写过一个万行级别的JavaScript项目,被那些奇怪的类型隐式转换折磨得夜不能寐之后,我终于决定给TypeScript一个机会。结果嘛——真香。

这篇文章就是想把我从”TypeScript是什么”到”能写简单项目”的过程完整记录下来,希望你少走一些弯路。

TypeScript类型系统配图 - 基础类型与接口实战示例

环境搭建:5分钟启动TypeScript

安装Node.js

TypeScript运行在Node.js环境中,所以第一步是安装Node.js。

Windows/macOS用户:

直接去Node.js官网下载LTS版本,安装过程没什么坑,一路下一步就行。装完之后打开终端验证:

bash

node --version
npm --version

Linux用户:

bash

# Ubuntu/Debian
sudo apt update
sudo apt install nodejs npm

# 验证安装
node --version
npm --version

安装TypeScript编译器

有了npm之后,安装TypeScript就是一行命令的事:

bash

npm install -g typescript

验证安装:

bash

tsc --version
# 应该输出类似:Version 5.4.3

第一个TypeScript程序

创建一个文件夹,比如叫ts-hello,然后在里面新建一个文件hello.ts

typescript

// hello.ts
function greet(name: string): string {
    return `你好,${name}!欢迎学习TypeScript。`;
}

const message = greet("小明");
console.log(message);

注意这个.ts后缀,这就是TypeScript文件的标志。

然后在终端里编译它:

bash

tsc hello.ts

执行完你会发现多了一个hello.js文件。没错,TypeScript代码需要编译成JavaScript才能运行。运行编译结果:

bash

node hello.js
# 输出:你好,小明!欢迎学习TypeScript。

使用配置文件

项目里文件多了,每次手动tsc 文件名就太麻烦了。这时候需要一个tsconfig.json配置文件。

bash

tsc --init

这会在当前目录生成一个默认配置文件。打开它,你会看到很多选项,这里先关注几个常用的:

json

{
    "compilerOptions": {
        "target": "ES2020",          // 编译目标JavaScript版本
        "module": "commonjs",         // 模块系统
        "outDir": "./dist",          // 编译输出目录
        "rootDir": "./src",          // 源码目录
        "strict": true,              // 开启严格模式(建议!)
        "esModuleInterop": true,      // 允许默认导入
        "skipLibCheck": true          // 跳过库文件检查
    },
    "include": ["src/**/*"],         // 要编译的文件
    "exclude": ["node_modules"]     // 排除的文件
}

配置好之后,把你的.ts文件放到src目录,执行tsc就会自动编译所有文件到dist目录。

基础类型:TypeScript的核心

这是TypeScript最重要的地方。类型系统让代码在写的时候就能发现错误,而不是等到运行时才崩溃。

基础类型有哪些

JavaScript的那些基础类型TypeScript都支持,只是在写法上有点区别:

typescript

// 字符串
let name: string = "张三";
let greeting: string = `你好,${name}`;

// 数字
let age: number = 25;
let price: number = 99.9;

// 布尔值
let isStudent: boolean = true;

// 数组
let numbers: number[] = [1, 2, 3, 4, 5];
let names: Array<string> = ["小明", "小红", "小刚"];  // 另一种写法

// 元组:固定长度和类型的数组
let person: [string, number] = ["张三", 25];

// 枚举:一组有名字的常量
enum Status {
    Pending,    // 0
    Active,     // 1
    Completed   // 2
}
let currentStatus: Status = Status.Active;

// 任意类型:当你真的不确定是什么类型时
let anything: any = 4;
anything = "hello";
anything = true;

// void:表示没有返回值
function logMessage(): void {
    console.log("这是一条日志");
}

// null 和 undefined
let n: null = null;
let u: undefined = undefined;

类型注解和类型推断

TypeScript有两种方式确定类型:

类型注解:你手动告诉编译器这是什么类型

typescript

let count: number = 10;
let username: string = "admin";

类型推断:TypeScript根据上下文自动推断类型

typescript

let count = 10;        // TypeScript推断这是number类型
let username = "admin"; // TypeScript推断这是string类型

// 后续再赋值其他类型会报错
count = "hello";  // 错误!count只能是number

实际开发中,没必要每个变量都写类型注解,TypeScript够聪明的时候会自己推断。但函数参数、返回值、复杂对象这些地方,类型注解是必要的。

接口:定义对象的形状

接口是TypeScript最强大的特性之一,它让你定义一个对象的结构:

typescript

// 定义一个用户接口
interface User {
    id: number;
    username: string;
    email: string;
    age?: number;           // 可选属性
    readonly createdAt: Date;  // 只读属性
}

// 创建符合接口的用户对象
const user: User = {
    id: 1,
    username: "zhangsan",
    email: "zhangsan@example.com",
    age: 25,
    createdAt: new Date()
};

// 缺少必填属性会报错
const badUser: User = {
    id: 2,
    username: "lisi"
    // 错误!缺少email和createdAt
};

// 尝试修改只读属性会报错
user.createdAt = new Date();  // 错误!createdAt是只读的

接口还能定义方法:

typescript

interface Calculator {
    (a: number, b: number): number;  // 函数的形状
    description: string;
}

const add: Calculator = (a, b) => a + b;
add.description = "加法计算器";

联合类型和交叉类型

有时候一个变量可能有多种类型,这时候用联合类型:

typescript

// 联合类型:可以是字符串或数字
let id: string | number;
id = "123";    // OK
id = 123;      // OK
id = true;     // 错误!布尔值不行

// 用联合类型限制函数参数
function printId(id: string | number): void {
    if (typeof id === "string") {
        console.log(`字符串ID: ${id.toUpperCase()}`);
    } else {
        console.log(`数字ID: ${id.toFixed(2)}`);
    }
}

交叉类型:把多个类型合并成一个:

typescript

interface Person {
    name: string;
    age: number;
}

interface Employee {
    company: string;
    salary: number;
}

// 交叉类型:既是Person又是Employee
type Worker = Person & Employee;

const worker: Worker = {
    name: "张三",
    age: 30,
    company: "某科技公司",
    salary: 15000
};

函数:参数和返回值的类型

TypeScript对函数的类型检查非常严格,这是好事。

函数类型表达式

typescript

// 声明一个函数类型
type AddFunction = (a: number, b: number) => number;

// 实现这个函数
const add: AddFunction = (x, y) => x + y;

// 箭头函数写法
const multiply: AddFunction = (x, y) => x * y;

可选参数和默认参数

typescript

// 可选参数:参数名后面加?
function buildName(firstName: string, lastName?: string): string {
    if (lastName) {
        return `${firstName} ${lastName}`;
    }
    return firstName;
}

console.log(buildName("张"));        // "张"
console.log(buildName("张", "三"));   // "张三"

// 默认参数:直接在参数列表赋值
function greet(name: string, greeting: string = "你好"): string {
    return `${greeting},${name}!`;
}

console.log(greet("小明"));          // "你好,小明!"
console.log(greet("小明", "早上好")); // "早上好,小明!"

剩余参数

typescript

// 收集剩余参数为数组
function sum(...numbers: number[]): number {
    return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3));        // 6
console.log(sum(1, 2, 3, 4, 5)); // 15

函数重载

同一个函数名,根据不同的参数类型返回不同类型:

typescript

// 函数重载签名
function reverse(x: string): string;  // 输入字符串返回字符串
function reverse(x: number): number;   // 输入数字返回数字
function reverse(x: string | number): string | number {
    if (typeof x === "string") {
        return x.split("").reverse().join("");
    }
    return Number(x.toString().split("").reverse().join(""));
}

console.log(reverse("hello"));  // "olleh"
console.log(reverse(12345));    // 54321

泛型:让类型灵活起来

泛型是TypeScript最难但最有用的部分。简单说,泛型就是”类型的变量”。

为什么需要泛型

先看一个没有泛型的例子:

typescript

// 这个函数返回数组的第一个元素
function getFirst(arr: any[]): any {
    return arr[0];
}

const num = getFirst([1, 2, 3]);    // 返回any类型
const str = getFirst(["a", "b"]);   // 返回any类型

// 调用时丢失了具体类型信息
num.toFixed(2);  // 编译器不知道num是number,可能报错

用泛型重写:

typescript

// T是类型参数,在调用时自动推断
function getFirst<T>(arr: T[]): T | undefined {
    return arr[0];
}

const num = getFirst([1, 2, 3]);        // 推断T为number
const str = getFirst(["a", "b"]);       // 推断T为string

num.toFixed(2);   // OK!TypeScript知道num是number
str.toUpperCase(); // OK!TypeScript知道str是string

泛型约束

有时候需要对泛型类型进行限制:

typescript

// 约束T必须有length属性
interface HasLength {
    length: number;
}

function logLength<T extends HasLength>(arg: T): void {
    console.log(`长度是:${arg.length}`);
}

logLength("hello");      // OK,字符串有length
logLength([1, 2, 3]);    // OK,数组有length
logLength({ length: 10 }); // OK,对象有length
logLength(123);          // 错误!数字没有length

多类型参数

typescript

// 交换两个变量的值
function swap<T, U>(tuple: [T, U]): [U, T] {
    return [tuple[1], tuple[0]];
}

const result = swap([1, "hello"]);
console.log(result);  // ["hello", 1]

实用技巧:工程实践

类型守卫

类型守卫让你在条件判断中缩小变量类型范围:

typescript

interface Cat {
    meow(): void;
}

interface Dog {
    bark(): void;
}

function isCat(animal: Cat | Dog): animal is Cat {
    return (animal as Cat).meow !== undefined;
}

function speak(animal: Cat | Dog) {
    if (isCat(animal)) {
        animal.meow();  // TypeScript知道这是Cat
    } else {
        animal.bark();  // TypeScript知道这是Dog
    }
}

keyof和索引类型

typescript

interface User {
    id: number;
    name: string;
    email: string;
}

// keyof User 等于 "id" | "name" | "email"
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const user: User = {
    id: 1,
    name: "张三",
    email: "zhangsan@example.com"
};

const userName = getProperty(user, "name");    // string
const userId = getProperty(user, "id");        // number
const userAge = getProperty(user, "age");      // 错误!"age"不在User中

工具类型

TypeScript内置了很多有用的工具类型:

typescript

interface User {
    id: number;
    name: string;
    email: string;
}

// Partial:所有属性变为可选
type PartialUser = Partial<User>;
// 等于 { id?: number; name?: string; email?: string; }

// Required:所有属性变为必填
type RequiredUser = Required<User>;

// Pick:挑选部分属性
type UserPreview = Pick<User, "id" | "name">;
// 等于 { id: number; name: string; }

// Omit:排除部分属性
type UserWithoutEmail = Omit<User, "email">;
// 等于 { id: number; name: string; }

// Record:创建指定键值对类型
type UserMap = Record<string, User>;
const users: UserMap = {
    "zhangsan": { id: 1, name: "张三", email: "zhangsan@example.com" },
    "lisi": { id: 2, name: "李四", email: "lisi@example.com" }
};

// Exclude:排除联合类型中的某些类型
type Status = "pending" | "active" | "completed" | "failed";
type ActiveStatus = Exclude<Status, "pending" | "failed">;
// 等于 "active" | "completed"

实战:一个简单的任务列表

学完上面的知识,来动手写个小项目吧。

项目结构

plaintext

task-app/
├── src/
│   ├── types.ts      # 类型定义
│   ├── task.ts        # 任务类
│   ├── taskList.ts    # 任务列表
│   └── index.ts       # 入口文件
├── dist/              # 编译输出
├── tsconfig.json
└── package.json

types.ts:类型定义

typescript

// 任务状态枚举
export enum TaskStatus {
    Todo = "todo",
    InProgress = "in_progress",
    Done = "done"
}

// 任务接口
export interface Task {
    id: string;
    title: string;
    description?: string;
    status: TaskStatus;
    createdAt: Date;
    completedAt?: Date;
}

// 创建任务的输入类型
export interface CreateTaskInput {
    title: string;
    description?: string;
}

// 更新任务的输入类型
export interface UpdateTaskInput {
    title?: string;
    description?: string;
    status?: TaskStatus;
}

task.ts:任务类

typescript

import { Task, TaskStatus, CreateTaskInput, UpdateTaskInput } from "./types";

export class TaskItem implements Task {
    id: string;
    title: string;
    description?: string;
    status: TaskStatus;
    createdAt: Date;
    completedAt?: Date;

    constructor(input: CreateTaskInput) {
        this.id = this.generateId();
        this.title = input.title;
        this.description = input.description;
        this.status = TaskStatus.Todo;
        this.createdAt = new Date();
    }

    private generateId(): string {
        return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    }

    update(input: UpdateTaskInput): void {
        if (input.title !== undefined) {
            this.title = input.title;
        }
        if (input.description !== undefined) {
            this.description = input.description;
        }
        if (input.status !== undefined) {
            this.status = input.status;
            if (input.status === TaskStatus.Done) {
                this.completedAt = new Date();
            } else {
                this.completedAt = undefined;
            }
        }
    }

    toString(): string {
        const statusText = {
            [TaskStatus.Todo]: "待办",
            [TaskStatus.InProgress]: "进行中",
            [TaskStatus.Done]: "已完成"
        };
        return `[${statusText[this.status]}] ${this.title}`;
    }
}

taskList.ts:任务列表管理

typescript

import { Task, TaskStatus, CreateTaskInput, UpdateTaskInput } from "./types";
import { TaskItem } from "./task";

export class TaskList {
    private tasks: Map<string, TaskItem> = new Map();

    create(input: CreateTaskInput): Task {
        const task = new TaskItem(input);
        this.tasks.set(task.id, task);
        return task;
    }

    findById(id: string): Task | undefined {
        return this.tasks.get(id);
    }

    findAll(): Task[] {
        return Array.from(this.tasks.values());
    }

    findByStatus(status: TaskStatus): Task[] {
        return this.findAll().filter(task => task.status === status);
    }

    update(id: string, input: UpdateTaskInput): Task | undefined {
        const task = this.tasks.get(id);
        if (!task) {
            return undefined;
        }
        task.update(input);
        return task;
    }

    delete(id: string): boolean {
        return this.tasks.delete(id);
    }

    clear(): void {
        this.tasks.clear();
    }

    getStats(): { total: number; todo: number; inProgress: number; done: number } {
        const all = this.findAll();
        return {
            total: all.length,
            todo: all.filter(t => t.status === TaskStatus.Todo).length,
            inProgress: all.filter(t => t.status === TaskStatus.InProgress).length,
            done: all.filter(t => t.status === TaskStatus.Done).length
        };
    }
}

index.ts:入口文件

typescript

import { TaskStatus } from "./types";
import { TaskList } from "./taskList";

// 创建任务列表实例
const taskList = new TaskList();

// 添加一些任务
const task1 = taskList.create({
    title: "学习TypeScript",
    description: "完成TypeScript入门教程"
});

const task2 = taskList.create({
    title: "搭建项目框架",
    description: "使用React + TypeScript"
});

const task3 = taskList.create({
    title: "写单元测试"
});

// 更新任务状态
taskList.update(task1.id, { status: TaskStatus.InProgress });

// 打印所有任务
console.log("=== 所有任务 ===");
taskList.findAll().forEach(task => {
    const taskItem = taskList.findById(task.id)!;
    console.log(taskItem.toString());
});

// 打印统计
console.log("\n=== 任务统计 ===");
const stats = taskList.getStats();
console.log(`总计:${stats.total}`);
console.log(`待办:${stats.todo}`);
console.log(`进行中:${stats.inProgress}`);
console.log(`已完成:${stats.done}`);

// 删除一个任务
taskList.delete(task3.id);
console.log("\n删除任务后:", taskList.findAll().length, "个任务");

编译运行:

bash

tsc
node dist/index.js

输出:

plaintext

=== 所有任务 ===
[进行中] 学习TypeScript
[待办] 搭建项目框架
[待办] 写单元测试

=== 任务统计 ===
总计:3
待办:2
进行中:1
已完成:0

删除任务后: 2 个任务

总结

好,写到这里你应该对TypeScript有了比较完整的认识。让我简单总结一下今天学的内容:

  1. 环境搭建:Node.js + TypeScript编译器,5分钟搞定
  2. 基础类型:string、number、boolean、数组、枚举、接口
  3. 函数类型:参数类型、返回值类型、可选参数、默认参数、泛型
  4. 泛型:让类型像变量一样灵活使用
  5. 实用技巧:类型守卫、keyof、工具类型
  6. 实战项目:一个完整的任务列表应用

坦白说,TypeScript的入门门槛确实比JavaScript高一些,但一旦你习惯了写类型注解,你会发现代码质量提升了一大截——Bug少了,代码更好维护了,IDE的智能提示也更准确了。

如果你之前一直在写JavaScript,强烈建议你新项目尝试用TypeScript,或者把旧项目一点点迁移过去。不需要一步到位,可以先从简单的地方开始加类型注解,慢慢来。

关于内链方面,你可以继续学习Vue3 Composition API实战教程或者RESTful API接口设计规范,这些都是前端开发者的必备技能。

常见问题

Q:TypeScript和JavaScript有什么区别?

A:TypeScript是JavaScript的超集,添加了类型系统和面向对象的特性。TypeScript代码需要编译成JavaScript才能在浏览器或Node.js中运行。简单说,TypeScript = JavaScript + 类型系统 + 编译。

Q:TypeScript难学吗?

A:对于有JavaScript基础的开发者来说,上手TypeScript并不难。最基本的类型注解几乎不需要额外学习。最难的部分是泛型和高级类型,但这些可以在实际项目中慢慢深入。

Q:所有项目都需要用TypeScript吗?

A:不一定。小型项目、原型项目用JavaScript更灵活。但中大型项目、团队协作项目、需要长期维护的项目,用TypeScript能显著提升代码质量和可维护性。

Q:如何从JavaScript迁移到TypeScript?

A:可以把tsconfig.json中的strict设置为false,然后逐个文件把.js改成.ts。TypeScript会兼容大部分JavaScript语法,然后慢慢加上类型注解。

希望这篇教程对你有帮助。如果有问题或建议,欢迎在评论区交流!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注