杰瑞科技汇

TypeScript教程该怎么学?入门到进阶怎么规划?

TypeScript 教程:从入门到精通

目录

  1. 什么是 TypeScript?
  2. 为什么使用 TypeScript?
  3. 环境搭建
  4. 基础类型
    • 原始类型
    • 数组
    • 元组
    • 枚举
    • Any & Unknown
    • Void & Never & Null & Undefined
    • Object
  5. 函数
    • 函数声明与表达式
    • 可选参数与默认参数
    • 剩余参数
    • 函数重载
  6. 接口
    • 对象类型
    • 可选属性
    • 只读属性
    • 函数类型
    • 可索引类型
    • 类类型实现
    • 成员可见性:public, private, protected
    • 存取器
    • 静态成员
    • 抽象类
  7. 泛型
    • 简单泛型
    • 泛型接口
    • 泛型类
    • 泛型约束
  8. 类型推断与断言
    • 类型推断
    • 类型断言
    • 非空断言
  9. 高级类型
    • 交叉类型
    • 联合类型
    • 类型别名
    • 类型守卫
    • 映射类型
    • 条件类型
  10. 项目配置与工具
    • tsconfig.json 配置文件详解
    • 使用 tsc 编译
    • 与代码编辑器(如 VS Code)的集成
    • 在项目中引入(React, Vue, Node.js)
  11. 学习资源与最佳实践

什么是 TypeScript?

TypeScript 是由微软开发的开源编程语言,它是 JavaScript 的一个超集

TypeScript教程该怎么学?入门到进阶怎么规划?-图1
(图片来源网络,侵删)
  • 超集:意味着任何合法的 JavaScript 代码也都是合法的 TypeScript 代码。
  • 静态类型:TypeScript 在编译时进行类型检查,而不是在运行时,你可以在编写代码时就发现类型错误,而不是等到用户在浏览器中运行时才发现。
  • 编译成 JavaScript:TypeScript 代码不能直接在浏览器或 Node.js 中运行,你需要使用 TypeScript 编译器(tsc)将其编译成纯 JavaScript 代码。

TypeScript = JavaScript + 静态类型系统


为什么使用 TypeScript?

使用 TypeScript 带来了诸多好处:

  1. 类型安全:在开发阶段就能捕获大量因类型不匹配导致的错误,减少了运行时崩溃的风险。
  2. 更好的代码提示和自动补全:现代编辑器(如 VS Code)可以根据类型信息提供极其智能的代码提示,提高开发效率。
  3. 提高代码可读性和可维护性:类型就像一份“活文档”,让代码的结构和意图更加清晰,便于团队协作和后期维护。
  4. 支持现代和未来的 JavaScript 特性:TypeScript 支持最新的 ECMAScript 标准,并可以编译成兼容旧环境的代码。
  5. 强大的重构工具:基于类型的重构(如重命名变量、修改函数签名)更加安全可靠。

环境搭建

  1. 安装 Node.js:TypeScript 依赖于 Node.js 环境,请确保你已经安装了 Node.js

  2. 安装 TypeScript:通过 npm(Node 包管理器)全局安装 TypeScript。

    TypeScript教程该怎么学?入门到进阶怎么规划?-图2
    (图片来源网络,侵删)
    npm install -g typescript
  3. 验证安装

    tsc --version
  4. 创建第一个项目

    # 创建一个新目录
    mkdir my-ts-project
    cd my-ts-project
    # 初始化 npm 项目(会生成 package.json)
    npm init -y
    # 创建一个 TypeScript 配置文件
    tsc --init

    执行 tsc --init 后,会生成一个 tsconfig.json 文件,这是 TypeScript 项目的配置核心。


基础类型

原始类型

// string
let message: string = "Hello, TypeScript!";
// number (支持整数、浮点数、NaN、Infinity)
let age: number = 25;
// boolean
let isActive: boolean = true;
// null 和 undefined
let notFound: null = null;
let something: undefined = undefined;
// 注意:默认情况下 null 和 undefined 是所有类型的子类型

数组

// 方式一:在元素类型后接 []
let list1: number[] = [1, 2, 3];
// 方式二:使用泛型 Array<元素类型>
let list2: Array<string> = ["a", "b", "c"];
// 数组中也可以是联合类型
let list3: (number | string)[] = [1, "two", 3];

元组

元组类型允许表示一个已知元素数量和类型的数组。

TypeScript教程该怎么学?入门到进阶怎么规划?-图3
(图片来源网络,侵删)
// 定义一个包含 string 和 number 的元组
let x: [string, number];
// 初始化
x = ["hello", 10]; // 正确
// x = [10, "hello"]; // 错误:类型不匹配
// 访问元素
console.log(x[0].substring(0, 1)); // 正确
// console.log(x[1].substring(0, 1)); // 错误:number 类型没有 substring 方法

枚举

枚举是对 JavaScript 标准功能的一个补充,它允许你为一组数值赋予友好的名字。

// 数字枚举(默认从 0 开始)
enum Color {
  Red,
  Green,
  Blue,
}
let c: Color = Color.Green; // c 的值是 1
// 也可以手动赋值
enum Status {
  Pending = 1,
  Success = 2,
  Failed = 3,
}
// 字符串枚举
enum Message {
  Success = "SUCCESS",
  Error = "ERROR",
}

Any & Unknown

  • any: 允许你为任何类型赋值,也允许将任何类型赋给它,它会绕过 TypeScript 的类型检查,在你不确定类型时使用。

    let notSure: any = 4;
    notSure = "maybe a string";
    notSure = false;
  • unknown: 是 any 的一个更安全的类型,任何类型的值都可以赋给 unknownunknown 类型的值不能随意赋给其他类型,在使用前,你必须进行类型检查。

    let userInput: unknown;
    userInput = 5;
    userInput = "hello";
    let userName: string;
    // userName = userInput; // 错误:不能将 unknown 类型赋给 string
    if (typeof userInput === "string") {
      userName = userInput; // 正确
    }

Void & Never & Null & Undefined

  • void: 通常用于表示一个函数没有返回值。

    function logMessage(msg: string): void {
      console.log(msg);
    }
  • never: 表示永远不会有返回值的函数,通常用于抛出异常或无限循环的函数。

    function error(message: string): never {
      throw new Error(message);
    }
  • nullundefined: 和原始类型一样,它们也是有效的类型。

Object

object 类型表示非原始类型,也就是除 number, string, boolean, symbol, nullundefined 之外的类型。

let obj: object;
obj = { name: "Alice" };
// obj = 123; // 错误
// 更精确地定义对象类型,使用接口或类型别名(后面会讲)
let person: { name: string; age: number };
person = { name: "Bob", age: 30 };

函数

函数声明与表达式

// 函数声明
function add(x: number, y: number): number {
  return x + y;
}
// 函数表达式
const myAdd = function(x: number, y: number): number {
  return x + y;
};
// 箭头函数
const multiply = (x: number, y: number): number => {
  return x * y;
};

可选参数与默认参数

// 可选参数必须在必选参数之后
function buildName(firstName: string, lastName?: string): string {
  if (lastName) {
    return `${firstName} ${lastName}`;
  }
  return firstName;
}
console.log(buildName("John")); // John
console.log(buildName("John", "Doe")); // John Doe
// 默认参数
function greet(name: string, greeting: string = "Hello"): string {
  return `${greeting}, ${name}!`;
}
console.log(greet("Alice")); // Hello, Alice!
console.log(greet("Bob", "Hi")); // Hi, Bob!

剩余参数

function sum(...numbers: number[]): number {
  return numbers.reduce((total, current) => total + current, 0);
}
console.log(sum(1, 2)); // 3
console.log(sum(1, 2, 3, 4)); // 10

函数重载

TypeScript 允许你定义多个函数签名,以便对同一个函数进行不同参数类型的调用。

// 定义重载签名
function processInput(value: string): string;
function processInput(value: number): number;
function processInput(value: boolean): boolean;
// 实现签名
function processInput(value: string | number | boolean): string | number | boolean {
  if (typeof value === "string") {
    return value.toUpperCase();
  } else if (typeof value === "number") {
    return value * 2;
  } else {
    return !value;
  }
}
// 正确调用
let result1 = processInput("hello"); // string
let result2 = processInput(10);      // number
let result3 = processInput(true);    // boolean

接口

接口是命名对象类型的一种方式,它非常灵活。

对象类型

interface User {
  name: string;
  age: number;
}
function printUser(user: User) {
  console.log(`User: ${user.name}, Age: ${user.age}`);
}
printUser({ name: "Charlie", age: 35 });

可选属性

interface Car {
  brand: string;
  model: string;
  year?: number; // 可选属性
}
const myCar: Car = { brand: "Toyota", model: "Camry" };
const anotherCar: Car = { brand: "Honda", model: "Civic", year: 2025 };

只读属性

interface Point {
  readonly x: number;
  readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
// p1.x = 5; // 错误:无法分配到 "x" ,因为它是只读属性

函数类型

interface SearchFunc {
  (source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
  return src.indexOf(sub) > -1;
};

可索引类型

// 支持数字索引
interface StringArray {
  [index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
// 支持字符串索引
interface StringDictionary {
  [key: string]: number;
}
let myDict: StringDictionary = { age: 25, score: 98 };

类类型实现

interface ClockInterface {
  currentTime: Date;
  setTime(d: Date): void;
}
class Clock implements ClockInterface {
  currentTime: Date = new Date();
  setTime(d: Date) {
    this.currentTime = d;
  }
  constructor(h: number, m: number) {}
}

TypeScript 为 ES6 的类添加了类型声明和一些新的特性。

成员可见性:public, private, protected

  • public (默认): 成员在类内部和外部都可以访问。
  • private: 成员只能在类内部访问。
  • protected: 成员只能在类内部和其子类中访问。
class Animal {
  public name: string; // 可以省略 public
  private age: number;
  protected species: string;
  constructor(name: string, age: number, species: string) {
    this.name = name;
    this.age = age;
    this.species = species;
  }
  public makeSound() {
    console.log(`${this.name} makes a sound.`);
  }
  private getAge() {
    return this.age;
  }
}
class Dog extends Animal {
  constructor(name: string, age: number) {
    super(name, age, "Canis lupus");
  }
  public bark() {
    // this.age; // 错误:private 属性不能在子类中访问
    this.species; // 正确:protected 属性可以在子类中访问
    console.log(`${this.name} barks!`);
  }
}
const myDog = new Dog("Rex", 5);
myDog.makeSound(); // Rex makes a sound.
myDog.bark();      // Rex barks!
// myDog.getAge(); // 错误:private 方法不能在外部访问

存取器

使用 getset 来拦截对对象属性的访问。

class Employee {
  private _fullName: string = "";
  get fullName(): string {
    return this._fullName;
  }
  set fullName(newName: string) {
    if (newName.length > 0) {
      this._fullName = newName;
    } else {
      throw new Error("Name cannot be empty");
    }
  }
}
let employee = new Employee();
employee.fullName = "Jane Doe";
console.log(employee.fullName); // Jane Doe
// employee.fullName = ""; // 抛出错误

静态成员

使用 static 关键字定义属于类本身而不是类实例的成员。

class MathUtils {
  static PI: number = 3.14159;
  static calculateCircleArea(radius: number): number {
    return this.PI * radius * radius;
  }
}
// console.log(MathUtils.PI);
// console.log(MathUtils.calculateCircleArea(5));

抽象类

抽象类作为其他类的基类使用,不能被实例化,它可能包含未实现的方法(抽象方法)。

abstract class Department {
  constructor(public name: string) {}
  printName(): void {
    console.log("Department name: " + this.name);
  }
  abstract printMeeting(): void; // 必须在子类中实现
}
class AccountingDepartment extends Department {
  constructor() {
    super("Accounting and Auditing"); // 在派生类构造函数中必须调用 super()
  }
  printMeeting(): void {
    console.log("The Accounting Department meets at 10am.");
  }
  generateReports(): void {
    console.log("Generating accounting reports...");
  }
}
// let department = new Department(); // 错误:不能创建抽象类的实例
let department = new AccountingDepartment();
department.printName(); // Department name: Accounting and Auditing
department.printMeeting(); // The Accounting Department meets at 10am.
department.generateReports(); // Generating accounting reports...

泛型

泛型是一种在定义函数、接口或类时,不预先指定具体的类型,而是在使用时再指定类型的特性,它提供了可重用性、类型安全和灵活性。

简单泛型

// 创建一个 identity 函数,它返回任何传入的类型
function identity<T>(arg: T): T {
  return arg;
}
// 使用方式1:明确指定类型
let output1 = identity<string>("myString");
let output2 = identity(42); // 使用方式2:类型推断,T 被推断为 number
console.log(output1); // myString
console.log(output2); // 42

泛型接口

interface GenericIdentityFn<T> {
  (arg: T): T;
}
let myIdentity: GenericIdentityFn<number> = identity;

泛型类

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
  constructor(zero: T, addFn: (x: T, y: T) => T) {
    this.zeroValue = zero;
    this.add = addFn;
  }
}
let myGenericNumber = new GenericNumber<number>(0, (x, y) => x + y);
console.log(myGenericNumber.add(5, 10)); // 15
let stringNumeric = new GenericNumber<string>("", (x, y) => x + y);
console.log(stringNumeric.add("hello, ", "world")); // hello, world

泛型约束

有时,我们希望泛型类型 T 拥有某些属性,这时可以使用 extends 关键字进行约束。

// 约束 T 必须有 length 属性
interface Lengthwise {
  length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length); // 现在我们知道 arg 有 .length 属性
  return arg;
}
loggingIdentity("Hello"); // 正确,string 有 length
loggingIdentity([1, 2, 3]); // 正确,array 有 length
// loggingIdentity(3); // 错误,number 没有 length 属性

类型推断与断言

类型推断

TypeScript 编译器会根据你提供的代码自动推断出类型,这减少了显式类型注解的需要。

let x = 3; // x 被推断为 number
let y = [1, "two", 3]; // y 被推断为 (number | string)[]
function getFirstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}
let first = getFirstElement([1, 2, 3]); // first 被推断为 number | undefined

类型断言

类型断言告诉编译器“我知道我在做什么,相信我,这个值是这个类型”,它不会进行数据转换或运行时检查,只是欺骗编译器。

语法1:<Type>value 语法2:value as Type (在 JSX 中必须使用这个)

let someValue: any = "this is a string";
// 方式1
let strLength1 = (<string>someValue).length;
// 方式2 (推荐,尤其在 TypeScript 3.7+ 和 React JSX 中)
let strLength2 = (someValue as string).length;

非空断言

在可能为 nullundefined 的值后面使用 ,告诉 TypeScript 这个值不可能是 nullundefined

function getNonNullElement<T>(arr: T[], index: number): T {
  // 如果不使用 !,TypeScript 会报错,因为 index 可能越界
  return arr[index]!;
}
const myArray = [1, 2, 3];
const firstElement = getNonNullElement(myArray, 0); // 1

高级类型

交叉类型

将多个类型合并成一个类型,常用于将多个对象的属性合并。

interface Person {
  name: string;
}
interface Loggable {
  log: () => void;
}
type PersonWithLog = Person & Loggable;
let p: PersonWithLog;
p = {
  name: "Alice",
  log: () => console.log("Logging..."),
};

联合类型

一个值可以是几种类型之一。

function printId(id: number | string) {
  console.log(id);
}
printId(101);
printId("202");

类型别名

使用 type 关键字给一个类型起个新名字,它和接口很像,但有一些区别(比如可以联合、交叉)。

type ID = number | string;
type User = {
  id: ID;
  name: string;
};
let user: User = { id: 123, name: "Bob" };
let anotherUser: User = { id: "abc-123", name: "Charlie" };

类型守卫

一种在运行时检查表达式类型的机制,以确保在某个代码块中,变量的类型是更具体的类型。

// typeof 守卫
function printValue(x: number | string) {
  if (typeof x === "string") {
    console.log(x.toUpperCase()); // 在这个块里,x 被视为 string
  } else {
    console.log(x.toFixed(2)); // 在这个块里,x 被视为 number
  }
}
// instanceof 守卫
class Dog {}
class Cat {}
function getPet(pet: Dog | Cat) {
  if (pet instanceof Dog) {
    console.log("It's a dog");
  } else {
    console.log("It's a cat");
  }
}

映射类型

基于一个现有类型,创建一个新类型,其属性是原类型的转换,将所有属性变为只读。

interface Person {
  name: string;
  age: number;
}
// 将 Person 的所有属性变为可选
type PartialPerson = {
  [P in keyof Person]?: Person[P];
};
// 将 Person 的所有属性变为只读
type ReadonlyPerson = {
  readonly [P in keyof Person]: Person[P];
};
const readOnlyPerson: ReadonlyPerson = { name: "David", age: 40 };
// readOnlyPerson.name = "Dave"; // 错误

条件类型

类似于 JavaScript 中的三元运算符,但作用于类型。

type ExtractType<T, U> = T extends U ? T : never;
type A = ExtractType<string | number | boolean, string>; // string
type B = ExtractType<string | number | boolean, number>; // number
type C = ExtractType<string | number | boolean, boolean>; // boolean
type D = ExtractType<string | number | boolean, Date>; // never
// 一个实际应用:NonNullable 类型
type NonNullable<T> = T extends null | undefined ? never : T;
type E = NonNullable<string | null | undefined>; // string

项目配置与工具

tsconfig.json 配置文件

这是 TypeScript 项目的灵魂,下面是一些常用选项:

{
  "compilerOptions": {
    // 目标 JavaScript 版本
    "target": "es2025",                           // 指定 ECMAScript 目标版本
    "module": "commonjs",                         // 指定模块代码生成方式
    "lib": ["es2025"],                            // 指定要包含在编译中的库文件
    // 模块解析策略
    "moduleResolution": "node",                   // 模块解析策略
    "baseUrl": "./",                              // 用于解析非相对模块名称的基本目录
    "paths": {                                    // 模块名到基于 baseUrl 的路径映射的列表
      "@/*": ["src/*"]
    },
    // 严格类型检查
    "strict": true,                               // 启用所有严格类型检查选项
    "noImplicitAny": true,                        // 在表达式和声明上有隐含的 any 类型时报错
    "strictNullChecks": true,                     // 启用严格的 null 检查
    "noImplicitThis": true,                       // 当 this 表达式的值为 any 类型的时候,生成一个错误
    "alwaysStrict": true,                         // 总是以严格模式解析并为每个源文件生成 "use strict" 语句
    // 额外检查
    "noUnusedLocals": true,                       // 若有未使用的局部变量则报错
    "noUnusedParameters": true,                   // 若有未使用的参数则报错
    "noImplicitReturns": true,                    // 不是函数的所有返回路径都有返回值时报错
    "noFallthroughCasesInSwitch": true,           // switch 语句必须有 default
    // 源码映射
    "sourceMap": true,                            // 生成相应的 '.map' 文件
    // 输出目录
    "outDir": "./dist",                           // 输出目录
    "rootDir": "./src",                           // 仅用来控制输出目录结构 - 输出目录会 mirrors 此处
    // 高级
    "esModuleInterop": true,                      // 启用 emitDecoratorMetadata
    "skipLibCheck": true,                         // 跳过对声明文件的类型检查
    "forceConsistentCasingInFileNames": true      // 禁止对同一文件使用不一致的大小写引用
  },
  "include": ["src/**/*"],                       // 指定要编译的文件
  "exclude": ["node_modules"]                    // 指定不编译的文件
}

使用 tsc 编译

在项目根目录下运行:

# 使用 tsconfig.json 的配置进行编译
tsc
# 监听模式,文件变化时自动重新编译
tsc --watch

与代码编辑器(如 VS Code)的集成

VS Code 对 TypeScript 有第一流的支持,只要你安装了 TypeScript 插件,它就能:

  • 实时显示类型错误。
  • 提供强大的代码提示和自动补全。
  • 快速跳转到定义。
  • 重构代码。

在项目中引入

  • React:

    1. npm install --save-dev typescript @types/react @types/react-dom
    2. .js 文件重命名为 .tsx(如果使用了 JSX)。
    3. 配置 tsconfig.json
  • Vue:

    1. npm install --save-dev typescript vue-tsc @vue/tsconfig
    2. 安装 Volar 插件(替代 Vetur)。
    3. 配置 tsconfig.json
  • Node.js:

    1. npm install --save-dev typescript @types/node
    2. 配置 tsconfig.jsontarget 可以是 es2025 或更高,module 通常设为 commonjs

学习资源与最佳实践

学习资源

最佳实践

  1. 开启 strict 模式:这是最有效的实践,它能帮你发现最多的潜在问题。
  2. 优先使用 interface 而不是 type:对于对象类型,interface 更易于扩展和合并。type 更适合联合、交叉等复杂类型。
  3. 尽可能使用 constlet:避免使用 var
  4. 避免使用 any:除非你真的需要绕过类型检查。unknown 是一个更安全的选择。
  5. 利用类型推断:不要过度使用类型注解,让 TypeScript 帮你做。
  6. 保持类型简单和明确:清晰的类型定义是代码可维护性的关键。
  7. 为第三方库安装类型定义:使用 npm install --save-dev @types/库名

希望这份教程能帮助你顺利入门 TypeScript!祝你学习愉快!

分享:
扫描分享到社交APP
上一篇
下一篇