一文带你轻松搞懂TypeScript

0. 先搞清楚:TypeScript 到底在解决什么?

你看,JavaScript 是个好东西——你把代码交给浏览器,它原样执行。但问题就出在"原样"两个字上。

function add(a, b) {
  return a + b;
}

add(2, 3);        // 5,没问题
add("2", 3);      // "23",咦?字符串拼接了
add(2, "3");      // "23",照样拼
add({}, 3);       // "[object Object]3",彻底乱套

函数 add 心里想的是"两个数相加",但 JavaScript 不管这些。你喂它什么,它就按自己的规则硬算。等到程序跑了一万次、在生产环境崩了,你才知道 a 不知道什么时候变成了一个对象。

TypeScript 干的事只有一件:在代码运行之前,替你把"这个变量应该是什么"钉死,并且检查你有没有违反自己的约定。

function add(a: number, b: number): number {
  return a + b;
}

add(2, 3);        // ✅
add("2", 3);      // ❌ 报错:Argument of type 'string' is not assignable to parameter of type 'number'

关键点:TypeScript 只在你写代码的时候报错,编译成 JavaScript 后这些检查全部消失。 它不让你半夜被 undefined is not a function 叫醒,就这么简单。

类型不是"额外工作",类型是"把你脑子里对数据的假设写出来"。你不写,这些假设就藏在某个角落,等到出事才暴露。


1. 类型注解与类型推断

1.1 显式注解(你亲口告诉编译器)

冒号 : 是"这个东西是什么类型"的记号。

let age: number = 18;
let name: string = "前端王睿";
let isStudent: boolean = true;
let hobbies: string[] = ["物理", "打鼓", "开锁"];

1.2 类型推断(编译器比你聪明,能猜出来)

很多时候你不用写类型,TypeScript 看一眼右边就能推断出来:

let age = 18;          // 推断为 number
let name = "前端王睿";      // 推断为 string
let nums = [1, 2, 3];  // 推断为 number[]

age = "十八";          // ❌ 报错:不能把 string 赋给 number

那什么时候该写、什么时候该让它推断? 一条简单规则:

  • 变量初始化时就赋值 → 让编译器推断,别多写
  • 函数参数、函数返回值、对象结构这些"对外契约" → 显式写出来,让读代码的人一眼看清边界。
// 参数和返回值都写出来,这是函数的"接口合同"
function greet(name: string): string {
  return `你好,${name}`;
}

2. 基本类型全家福

类型 说明 例子
number 所有数字(没有 int/float 之分) 42, 3.14, 0b1010
string 字符串 "hi", `值=${x}`
boolean 布尔 true, false
null 空值(已赋值但为空) let x = null
undefined 未定义(还没赋值) let x: number | undefined
any 退出类型检查(危险!) let x: any = 1
unknown 类型安全的 any(推荐) let x: unknown = 1
void 函数无返回值 function log(): void {}
never 永远不会发生的值 抛异常的函数
object 非原始类型 { a: 1 }

anyunknown 的区别(很多人栽在这)

any 等于对编译器说"别管了",它关掉了所有检查——这是把枪口对准自己:

let data: any = "字符串";
data.foo.bar.baz();   // 编译通过,但运行时必崩

unknown 是"我不知道是什么,但你用之前必须证明你知道":

let data: unknown = "字符串";

data.length;              // ❌ 报错:unknown 上不能直接访问属性
if (typeof data === "string") {
  data.length;            // ✅ 收窄后可用,data 被推断为 string
}

记住:能写 unknown 就不要写 any


3. 数组与元组

3.1 数组

let scores: number[] = [90, 85, 77];
let names: Array<string> = ["a", "b"];   // 另一种等价写法(泛型,见第6节)

3.2 元组(Tuple)——"位置固定、类型固定的小数组"

普通数组要求"所有元素同一类型"。但现实中很多东西是按位置区分的,比如坐标、键值对:

// 第一个是名字(string),第二个是分数(number)
let student: [string, number] = ["前端王睿", 95];

student[0].toUpperCase();  // ✅ string 方法可用
student[1].toFixed(1);     // ✅ number 方法可用
student = [95, "前端王睿"];     // ❌ 顺序错了,报错

4. 对象类型与接口(Interface)

4.1 内联对象类型

let person: { name: string; age: number } = {
  name: "前端王睿",
  age: 18
};

4.2 接口(命名的对象结构契约)

当同一个结构出现多次,就该给它起个名字——这就是 interface

interface Person {
  name: string;
  age: number;
  email?: string;        // ? 表示可选属性
  readonly id: number;   // readonly 表示创建后不能改
}

const p: Person = {
  name: "前端王睿",
  age: 18,
  id: 1
};

p.age = 19;              // ✅
p.id = 2;               // ❌ 报错:Cannot assign to 'id' because it is a read-only property
p.email = "f@x.com";    // ✅ 可选属性可以补上

4.3 接口可以"继承"——extends

interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

const d: Dog = { name: "旺财", breed: "柴犬" };  // 必须同时有 name 和 breed

4.4 类型别名 type 与接口 interface 怎么选?

两者大部分时候能互换:

type Person = { name: string; age: number };
interface Person2 { name: string; age: number }

区别(记住这几条就够了):

  • 接口可以被声明合并(多次声明同名接口会自动合并),type 不行。
  • type 能表示任何类型(联合、基础类型、元组等),接口主要用于对象。
  • 官方建议:描述对象/类的结构用 interface;其他复杂类型组合用 type
// 声明合并:同一个 interface 名字出现两次,自动合并
interface User { name: string; }
interface User { age: number; }
// 最终 User = { name: string; age: number }

5. 联合类型与交叉类型

5.1 联合类型(Union)——"或者是 A,或者是 B"

function printId(id: number | string) {
  console.log(`ID 是 ${id}`);
}

printId(101);       // ✅
printId("abc");     // ✅
printId(true);      // ❌ 既不是 number 也不是 string

但注意:联合类型下,你只能使用"所有候选类型都有的方法":

function printId(id: number | string) {
  id.toUpperCase();   // ❌ number 没有 toUpperCase,报错
  // 必须先收窄:
  if (typeof id === "string") {
    id.toUpperCase();  // ✅ 这里 id 被收窄为 string
  }
}

5.2 交叉类型(Intersection)——"既是 A,又是 B"

type A = { x: number };
type B = { y: number };
type C = A & B;        // 同时拥有 x 和 y

const c: C = { x: 1, y: 2 };

| 是"或者",& 是"并且"。


6. 泛型(Generics)——"类型的函数"

这是最让人头大的一节,但也是 TypeScript 最强大的地方。先别背定义,看问题。

你想写一个"返回数组第一个元素"的函数。问题是:数组里装什么,取决于调用时传什么。你不想为 number[]string[] 各写一个函数。

泛型就是"把类型当参数传进去"——就像函数把值当参数,泛型把类型当参数。

function first<T>(arr: T[]): T {
  return arr[0];
}

const a = first([1, 2, 3]);      // T 被推断为 number,a: number
const b = first(["x", "y"]);     // T 被推断为 string,b: string
const c = first([true, false]);  // T 被推断为 boolean,c: boolean

<T> 里的 T 是"类型变量"的惯例命名(Type 的首字母)。你可以叫 <Item><X>,但社区都用 T

6.1 泛型 + 约束(extends

有时候你想让泛型"至少是某种形状":

interface HasLength {
  length: number;
}

// T 必须拥有 length 属性
function logLength<T extends HasLength>(item: T): T {
  console.log(item.length);
  return item;
}

logLength("hello");     // ✅ string 有 length
logLength([1, 2, 3]);   // ✅ 数组有 length
logLength(123);         // ❌ number 没有 length

6.2 真实项目里泛型长什么样?

React 里最常见的 useState

// 简化版实现
function useState<T>(initial: T): [T, (next: T) => void] {
  let value = initial;
  const setter = (next: T) => { value = next; };
  return [value, setter];
}

const [count, setCount] = useState(0);   // T = number
const [name, setName] = useState("");    // T = string

泛型不是炫技。它就是"这个函数/类型能处理多种数据,但调用时一锤定音,全程类型安全"。你写一次,编译器替你守住所有调用点。


7. 类型收窄(Narrowing)与类型守卫

编译器再聪明,遇到 string | number 也会"分不清此刻到底是哪种"。收窄就是写一段代码,让编译器在某一块作用域里"锁定"具体类型。

7.1 typeof 守卫

function double(input: number | string) {
  if (typeof input === "number") {
    return input * 2;          // ✅ 这里 input 是 number
  }
  return input.repeat(2);      // ✅ 这里 input 是 string
}

7.2 in 守卫(判断对象有没有某个属性)

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim();             // ✅ 收窄为 Fish
  } else {
    animal.fly();              // ✅ 收窄为 Bird
  }
}

7.3 自定义类型守卫(is 关键字)

这是最帅的用法——你写一个返回 value is Type 的函数,编译器会信你:

function isString(value: unknown): value is string {
  return typeof value === "string";
}

function format(input: unknown) {
  if (isString(input)) {
    input.toUpperCase();   // ✅ input 被收窄为 string
  }
}

8. 类(Class)与访问修饰符

TypeScript 给 JavaScript 的类加了三道门:public(默认,谁都能进)、private(只有自己能进)、protected(自己和子类能进)。

class BankAccount {
  public owner: string;          // 公开
  private balance: number;       // 私有,外部拿不到
  protected id: number;          // 受保护,子类可继承

  constructor(owner: string, balance: number) {
    this.owner = owner;
    this.balance = balance;
    this.id = Math.random();
  }

  public deposit(amount: number): void {
    this.balance += amount;       // 内部可以操作 private
  }

  public getBalance(): number {
    return this.balance;          // 通过方法暴露,而不是暴露字段
  }
}

const acc = new BankAccount("前端王睿", 100);
acc.deposit(50);
console.log(acc.getBalance());    // 150
console.log(acc.balance);         // ❌ 报错:balance 是 private

为什么要有 private?因为"钱"的逻辑(存取、校验)应该锁在类内部。外部只能走你允许的门。这和现实里银行不让你直接进金库是一个道理。


9. 实用工具类型(Utility Types)——"类型层面的乐高"

TypeScript 内置了一批"对类型做加工"的工具。你不用自己从头拼,直接用。

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

// Partial:所有属性变可选(常用于"更新"接口)
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string; age?: number }

// Pick:只挑几个属性
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string }

// Omit:排除几个属性
type NewUser = Omit<User, "id">;
// { name: string; email: string; age: number }

// Readonly:全部只读
type FrozenUser = Readonly<User>;

// Record:构造"键→值"映射类型
type RoleMap = Record<string, string>;
const roles: RoleMap = { admin: "管理员", user: "普通用户" };

最常用的还有 Required(全部必填,与 Partial 相反)。这些不是魔法,它们是用第10节的条件类型实现的。


10. 条件类型与 infer(进阶)

条件类型像"三元表达式",但是给类型用的:

type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>;   // "yes"
type B = IsString<number>;   // "no"

infer 更妙——它能在条件类型里"抽取"出一部分类型。比如"取出数组元素的类型":

type ElementType<T> = T extends (infer E)[] ? E : T;

type X = ElementType<number[]>;   // number
type Y = ElementType<string>;     // string(不是数组,原样返回)

但实际上你不需要自己写这个——它已经被内置成 ReturnTypeParameters 等工具:

function getUser(id: number): { name: string; age: number } {
  return { name: "x", age: 1 };
}

type UserInfo = ReturnType<typeof getUser>;
// { name: string; age: number } —— 自动从函数推导出返回值类型

这一节进阶内容,第一遍看不懂很正常。困惑是理解的起点,不是你笨。先会用第9节的工具类型,等哪天你真的需要"动态算出一个类型",再回来啃 infer


11. 一个完整小例子:把前面全串起来

一个"给任务列表加标签"的迷你系统:

// 1. 用 interface 定义数据结构
interface Task {
  id: number;
  title: string;
  done: boolean;
  tags: string[];
}

// 2. 用泛型函数创建任务
function createTask<T extends Task>(data: T): T {
  console.log(`创建任务:${data.title}`);
  return data;
}

// 3. 用 Utility Type 做"更新"——Partial 让更新时只传要改的字段
function updateTask(task: Task, patch: Partial<Task>): Task {
  return { ...task, ...patch };
}

// 4. 类型守卫:判断是不是已完成
function isDone(task: Task): task is Task & { done: true } {
  return task.done === true;
}

// 运行
const t1 = createTask({ id: 1, title: "学 TypeScript", done: false, tags: ["学习"] });
const t2 = updateTask(t1, { done: true });     // 只改 done
if (isDone(t2)) {
  console.log(`${t2.title} 已完成 ✅`);
}

每一行都能对应到前面某一节。如果你能看着这个例子说出"这里用了第几节的什么",你就真懂了——不是知道名字,是理解它。


12. 你真懂没懂,做个自检

对着下面每条,能用自己的话 + 一个例子解释,才算过:

  1. 为什么 TypeScript 编译后类型就没了? —— 类型只为"写代码时"服务,运行时是纯 JS。
  2. anyunknown 差在哪? —— any 关检查,unknown 强制你先证明类型。
  3. 联合 | 和交叉 & 的区别? —— 或 / 且。
  4. 泛型解决了什么? —— 一份逻辑处理多种类型,且全程类型安全。
  5. 收窄为什么必要? —— 联合类型下编译器分不清,用 typeof/in/is 帮它锁定。
  6. Partial / Pick / Omit 用来干嘛? —— 在已有接口上做增删改,避免重复定义。

把每个例子亲手敲一遍,改一改、故意写错看报错。报错信息就是你最好的老师。别只看文章,去造它。


写这篇不是让你背下来,是让你下次写 TypeScript 时,能想起"这个类型到底在帮我省什么麻烦"。想清楚这个,剩下的查文档就行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容