TypeScript 继续学习(学习用)

一、为什么需要 TypeScript?

JavaScript 是动态类型语言,变量类型在运行时才能确定。这带来了灵活性,但也容易写出意外错误:比如把数字当作函数调用,或者访问对象上不存在的属性。

TypeScript = JavaScript + 静态类型系统 。在写代码时给变量标注类型,TS 编译器会在编译阶段检查类型是否匹配,将错误提前暴露出来。

另外,类型注解是一份天然的"文档",编辑器能利用它给出精准的智能提示,极大提升开发效率。

注意:TypeScript 的类型只在编译时存在,编译后的 JS 代码中全部类型会被擦除,不会影响运行。


二、基础类型:给数据贴上明确的标签

1. 原始类型

概念:与 JavaScript 的七种原始类型对应(string, number, boolean, null, undefined, symbol, bigint)。

为什么需要:让编译器知道变量只能赋特定类型值,防止意外。

typescript 复制代码
let name: string = "Alice";   // name 只能是字符串
let age: number = 30;         // age 只能是数字
let isDone: boolean = false;  // isDone 只能是布尔值
let u: undefined = undefined;
let n: null = null;
let sym: symbol = Symbol("key");
let big: bigint = 100n;

注意nullundefined 是所有类型的子类型(在 strictNullChecks 关闭时),但建议开启 strict 以强制区分,避免出错。

2. 数组

概念:存储一组相同类型的数据。

typescript 复制代码
let list: number[] = [1, 2, 3];         // 元素只能是数字
let list2: Array<number> = [1, 2, 3];  // 泛型写法,等价

原理number[] 语法更简洁,Array<number> 是使用泛型接口。两者编译结果相同。

3. 元组 (Tuple)

概念:一个已知元素数量和各自类型的数组,各位置类型可不同。

为什么需要:函数返回多个不同值、坐标点等固定结构。

typescript 复制代码
let tuple: [string, number] = ["Alice", 25];
// 访问 tuple[0] 是 string,tuple[1] 是 number
// 越界访问会报错,比如tuple[2]会报错
 
tuple.push(null)
console.log(tuple)// [ 'Alice', 25, null ]

注意 :元组长度是固定的,但 push 等操作目前无法被完全限制(历史遗留),尽量避免。

4. 枚举 (Enum)

概念:一组有名字的常量。默认数字枚举,也可以字符串枚举。

为什么需要:代替魔法数字或字符串,让代码更可读。

typescript 复制代码
enum Direction {
  Up,    // 0
  Down,  // 1
  Left,  // 2
  Right  // 3
}
let dir: Direction = Direction.Up;

字符串枚举不给数值,但每个成员必须初始化:

typescript 复制代码
enum Status {
  Active = "ACTIVE",
  Inactive = "INACTIVE"
}

原理 :编译后,枚举会生成一个双向映射的对象(数字枚举),字符串枚举则为单向对象。使用 const enum 编译时不生成对象,直接内联值,性能更优。
注意:枚举不是 TypeScript 的类型级概念,它同时产生类型和运行时值。在需要常量集合时使用,否则可用联合字符串类型替代。

5. any、unknown、void、never

这些类型具有特殊含义:

  • any :放弃类型检查,与普通 JS 一样。应当少用,因为它破坏了类型安全。
  • unknown :安全的"any"。表示任意类型,但使用前必须用类型守卫收窄
  • void :函数没有返回值(返回 undefined)。
  • never:永远不会有返回值的函数(总是抛出异常或死循环)。
typescript 复制代码
let x: any = 10;
x.toUpperCase(); // 编译器不报错,但运行时可能出错

let y: unknown = 10;
if (typeof y === "string") {
  y.toUpperCase(); // 必须检查类型后才能用
}

function warn(): void {
  console.log("warning");
}

function error(): never {
  throw new Error("出错啦!");
}

原理unknown 是类型安全的顶类型(所有类型都可以赋给它,但它是不可操作的),any 既是顶类型又是底类型(它可赋给任何类型,不检查),never 是底类型(所有类型的子类型,没有具体值)。这个层级叫做类型兼容性


三、接口(Interface)与类型别名(Type)

1. 接口 Interface

概念:定义一个对象的形状(有哪些属性,分别是什么类型)。

为什么需要:为对象结构提供契约,保证对象拥有规定的属性和方法。

typescript 复制代码
interface User {
  name: string;
  age: number;
  readonly id: number;  // 只读,创建后不能改
  email?: string;       // 可选属性
}
const user: User = { name: "Alice", age: 25, id: 1 };
 user.id = 2; // 报错!

原理 :TypeScript 采用结构类型系统(鸭子类型),只要对象的结构满足接口要求即可,不需要显式声明实现。

2. 类型别名 Type

概念:给一个类型起个新名字,可以是任意类型。

typescript 复制代码
type Point = {
  x: number;
  y: number;
};
type ID = string | number;

3. Interface 与 Type 的区别(面试重点)

Interface Type
扩展方式 extends 继承 交叉类型 &
重复定义 同名接口自动合并 同名 type 报错
适用范围 主要描述对象、函数等 任何类型(联合、元组、映射等)
性能 接口可以被缓存,更适合扩展 类型别名只是别名
typescript 复制代码
// 接口合并
interface A { x: number; }
interface A { y: number; }  // 合并 => { x: number; y: number; }

// 类型别名不支持合并
type B = { x: number; };
type B = { y: number; }; // 报错

建议 :优先使用 interface 描述对象形状,当需要联合类型、重命名基本类型、元组等时用 type


四、联合类型与交叉类型

1. 联合类型 (Union)

概念 :一个值可以是几种类型之一,用 | 分隔。

为什么需要 :处理不确定的类型,如 string | number 的 ID。

typescript 复制代码
let id: string | number = "abc";
id = 123; // 合法

注意 :联合类型只能访问所有类型共有的属性。要使用特有属性,需先类型收窄(见下节)。

2. 交叉类型 (Intersection)

概念 :将多个类型合并成一个类型,用 & 连接。

为什么需要:组合多个对象或接口的特性。

typescript 复制代码
interface A { a: number; }
interface B { b: string; }
type C = A & B; // { a: number; b: string; }

原理:交叉类型是求并集(需同时满足),联合类型是求或集(满足其中一个即可)。交叉常用于合并多个对象类型,如混入模式。

2.1. 同名属性类型冲突 → 推导为 never

这是最常见的报错来源。当两个类型中同名属性的类型没有交集 时,交叉后该属性的类型变成 never

typescript 复制代码
interface X {
  value: string;
}
interface Y {
  value: number;
}
type Z = X & Y;
// Z 的类型相当于 { value: string & number }  =>  { value: never }

原理string & number 没有哪个值能同时是 string 又是 number,所以交点为空,即 never。实际使用时,你会收到类型错误:

typescript 复制代码
const obj: Z = {
  value: "hello" // ❌ 报错:不能将类型 'string' 分配给类型 'never'
};

或者试图赋值时直接报错:

typescript 复制代码
const test: Z = { value: 1 }; // ❌ 同样错误

2.2. 函数类型的交叉

如果冲突发生在函数类型上,交叉会形成函数重载 ,这通常不会报错,但调用时可能会限制为 never(取决于参数)。

typescript 复制代码
type Fn1 = (x: string) => void;
type Fn2 = (x: number) => void;
type Fn = Fn1 & Fn2;
// 相当于 (x: string & number) => void => (x: never) => void

此时调用该函数:

typescript 复制代码
declare const fn: Fn;
fn("hello"); // ❌ 报错:类型 'string' 的参数不能赋给类型 'never'
fn(123);     // ❌ 同样错误

函数交叉通常用于重载签名,正确的做法是使用函数重载声明或泛型。


2.3. 对象类型与方法签名的冲突

typescript 复制代码
interface Logger {
  log(msg: string): void;
}
interface Printer {
  log(msg: number): void;
}
type Hybrid = Logger & Printer;
// log 的类型变成 ((msg: string) => void) & ((msg: number) => void)
// 即 (msg: string & number) => void  -> (msg: never) => void

调用时会报错,因为不能同时是 stringnumber


2.4. 使用条件类型检测 never

typescript 复制代码
type IsNever<T> = [T] extends [never] ? true : false;
type Test = IsNever<Z['value']>; // true

2.5. 何时交叉类型不会报错?

  • 同名属性的类型兼容(如 stringstring,或 {a:number}{b:string} 合并成 {a:number; b:string})。
  • 属性和方法签名兼容(同一类型或子类型)。
  • 使用交叉扩展已有接口(如 type Extended = Base & { extra: boolean })。

2.6. 总结:交叉类型报错的根本原因

交叉类型本质上求的是"类型的交集" ,当交集为空时(即无法找到一个值同时满足两边),就会表现为 never,从而导致赋值错误。这是 TypeScript 保证类型安全的重要机制------它不会让你创造"既要求是 string 又要求是 number"的变量,因为那不可能存在。


五、类型守卫与类型收窄

概念:TypeScript 会分析代码逻辑,在条件判断后自动缩小变量的类型范围。

为什么需要:安全地使用联合类型中的特定方法。

typescript 复制代码
function printId(id: string | number) {
  if (typeof id === "string") {
    // 这里 id 被收窄为 string
    console.log(id.toUpperCase());
  } else {
    // 这里 id 为 number
    console.log(id.toFixed(2));
  }
}

常用守卫

  • typeof:用于原始类型
  • instanceof:用于类实例
  • in:检查属性是否存在
  • 自定义类型谓词:arg is Type 形式
typescript 复制代码
interface Cat { meow(): void; }
interface Dog { bark(): void; }

function isCat(animal: Cat | Dog): animal is Cat {
  return (animal as Cat).meow !== undefined;
}
if (isCat(pet)) {
  pet.meow(); // 这里 pet 是 Cat
}

原理animal is Cat 是类型谓词,函数返回 true 时,TypeScript 会将参数类型收窄为 Cat。这是从任意联合类型中提取类型的重要手段。

1. 什么是类型谓词?

类型谓词是 TypeScript 中一种特殊的返回值类型,语法为 parameterName is Type。它用于自定义类型守卫 (User-Defined Type Guard)。当一个函数的返回类型是类型谓词时,TypeScript 会根据函数的 true/false 结果,在后续代码中自动收窄参数的类型。

简单说,就是告诉编译器:"如果这个函数返回 true,那么参数就是某个特定类型"。

2. 为什么需要自定义类型守卫?

TypeScript 的内置类型收窄(如 typeofinstanceofin)只能处理一些简单情况。当联合类型中的多个类型具有不同的结构时,我们可以自己编写更精确的判断函数。

例如, 有 CatDog 两个接口:

typescript 复制代码
interface Cat {
  meow(): void;
}
interface Dog {
  bark(): void;
}

现在有一个函数接收 Cat | Dog 类型的参数,我们想调用 meow(),但直接调用会报错,因为 Dog 上没有 meow 方法:

typescript 复制代码
function makeSound(animal: Cat | Dog) {
  animal.meow(); // ❌ 报错:类型"Cat | Dog"上不存在属性"meow"
}

我们需要先判断它是 Cat 还是 Dog。可以使用 in 操作符:

typescript 复制代码
if ("meow" in animal) {
  animal.meow(); // 这里 animal 被收窄为 Cat
}

但有时候判断逻辑更复杂,或者想要更清晰的语义,就可以使用自定义类型守卫。

3. 详解 isCat 函数

typescript 复制代码
function isCat(animal: Cat | Dog): animal is Cat {
  return (animal as Cat).meow !== undefined;
}
  • 参数类型animal: Cat | Dog,表示传入的 animal 可能是 Cat 或 Dog。
  • 返回类型animal is Cat,这是一个类型谓词。它告诉 TypeScript:"如果这个函数返回 true,那么参数 animal 就是 Cat 类型"。
  • 函数体(animal as Cat).meow !== undefined。先用 as Cat 进行类型断言,将 animal 当作 Cat 来访问 meow 属性,然后判断它是否不等于 undefined。如果是,说明 animal 拥有 meow 方法,推断它是 Cat。

为什么这样做?

我们无法直接访问联合类型中不共有的属性,所以用类型断言临时"假设"它是 Cat,然后检查特有属性 meow 是否存在。这比 in 操作符更显式,适合复杂检查。

4. 使用 isCat 后的效果

typescript 复制代码
function interact(animal: Cat | Dog) {
  if (isCat(animal)) {
    // 在这个分支里,animal 被自动收窄为 Cat
    animal.meow(); // ✅ 合法
  } else {
    // 这里 animal 被收窄为 Dog
    animal.bark(); // ✅ 合法
  }
}

正确和错误调用的例子:

typescript 复制代码
const cat: Cat = { meow: () => console.log("Meow!") };
const dog: Dog = { bark: () => console.log("Woof!") };

// ✅ 正确使用
if (isCat(cat)) {
  cat.meow(); // 输出 "Meow!"
}

if (!isCat(dog)) {
  dog.bark(); // 输出 "Woof!"
}

// ❌ 错误使用:不能将 Cat 赋给 Dog 或反过来
// isCat 只接受 Cat | Dog,传入其他类型会报错
 isCat(42); // 报错:类型"number"的参数不能赋给类型"Cat | Dog"的参数

注意 :自定义类型守卫只是编译时的类型收窄,不会在运行时保证类型安全。如果函数体实现有误(例如总是返回 true),可能会导致运行时错误。所以必须确保守卫函数的逻辑是正确的。


六、函数类型

概念:指定函数的参数类型和返回值类型。

为什么需要:明确调用方式,防止传入错误参数。

typescript 复制代码
function add(x: number, y: number): number {
  return x + y;
}
//正确调用
add(1, 2);         // 返回 3
add(0.5, -3);      // 返回 -2.5
add(100, 200);     // 返回 300

//错误调用及原因:
add(1);            // ❌ 缺少第二个参数
add(1, 2, 3);      // ❌ 多余参数
add("1", 2);       // ❌ 第一个参数类型错误:string 不能赋给 number
add(1, "2");       // ❌ 第二个参数类型错误
add(true, false);  // ❌ boolean 不能赋给 number
//解释:`add` 的参数和返回值都明确为 `number`,TypeScript 会严格检查参数数量和类型。多传或少传都会报错

// 可选参数与默认值
function greet(name: string, title?: string): string {
  return title ? `${title} ${name}` : name;
}
//正确调用:
greet("Alice");               // 返回 "Alice"
greet("Bob", "Dr.");          // 返回 "Dr. Bob"
greet("Charlie", "");         // 返回 "Charlie"(因为空字符串是 falsy,取 name)
greet("Diana", "Professor");  // 返回 "Professor Diana"

//错误调用:
greet();                      // ❌ 缺少必选参数 name
greet(123);                   // ❌ name 必须是 string
greet("Eve", "Ms.", "extra"); // ❌ 多余参数
greet(true, false);           // ❌ 类型错误
greet("Frank", 5);            // ❌ title 只能是 string 或 undefined
//解释:`title?` 表示可选参数,可以传递 `string` 或 `undefined`,但不能传其他类型。在调用时,可选参数可以省略,但必选参数 `name` 必须提供。

// 剩余参数
function sum(...nums: number[]): number {
  return nums.reduce((a, b) => a + b, 0);
}
//正确调用:
sum();            // 返回 0(空数组求和)
sum(1);           // 返回 1
sum(1, 2, 3);     // 返回 6
sum(10, 20, 30, 40); // 返回 100

//错误调用:
sum(1, 2, "3");   // ❌ "3" 不是 number
sum([1, 2, 3]);   // ❌ 传入的是数组,不是展开的数字列表
sum(null);        // ❌ null 不能赋给 number
sum(undefined);   // ❌ undefined 不能赋给 number
//解释:`...nums: number[]` 表示收集所有剩余参数到一个 `number[]` 数组。调用时必须传入零个或多个 `number`,不能传入数组整体(除非使用展开语法 `sum(...[1,2,3])`,但那是将数组展开成参数)。传入非数字类型会报错。

注意:可选参数必须放在必选参数之后。函数重载(多个函数头签名)可以精确描述多种调用方式。

typescript 复制代码
// 函数重载:声明多种调用形式
function combine(a: string, b: string): string;
function combine(a: number, b: number): number;
// 实现签名
function combine(a: any, b: any): any {
  return a + b;
}
combine("Hello ", "World"); // 返回 string
combine(1, 2);              // 返回 number
// combine("a", 2);         // 报错!没有匹配的重载

原理:重载声明只是类型层面的信息,实现签名必须兼容所有重载声明。编译器根据传入参数类型选择对应声明检查。

6.1、为什么要函数重载?

JavaScript 中经常有一些函数,根据传入参数的类型或数量,执行不同的逻辑,返回不同类型。例如 Array.prototype.slice

  • 不传参数时返回数组的浅拷贝(数组)
  • 传入数字时返回子数组(数组)

如果用 TypeScript 写这个函数,我们希望:

typescript 复制代码
function slice(): number[];
function slice(start: number): number[];
function slice(start: number, end: number): number[];

三种调用方式返回的都是 number[],但这里签名已经多了。再比如一个 padLeft,传入数字和字符串的行为完全不同:

typescript 复制代码
padLeft("hello", 10);   // 返回 "     hello",期望类型是 string
padLeft(123, "0");      // 返回 "00123",期望类型也是 string

虽然看起来简单,但当参数类型决定返回值类型,或者参数数量不同导致处理方式不同时,函数重载就能精确地捕获这些调用模式,提供更准确的类型检查。

没有重载,你只能写一个笼统的实现,用 any 或联合类型,这样会丢失调用时对参数约束的精度。


6.2、示例代码详解

6.2.1. 重载声明

来看回上面的代码

typescript 复制代码
function combine(a: string, b: string): string;
function combine(a: number, b: number): number;

这两行被称为重载签名 ,它们定义了 combine 函数的两种合法调用方式:

  • 传入两个 string,返回 string
  • 传入两个 number,返回 number

这两个签名只用于类型检查,不包含函数体。TypeScript 会记住这些调用模式。

6.2.2. 实现签名

typescript 复制代码
function combine(a: any, b: any): any {
  return a + b;
}

这是唯一真正的函数体,被称为实现签名 。它的参数类型和返回类型必须兼容 所有重载签名(通常用更宽泛的类型如 any、联合类型等来涵盖所有可能)。

在这里,abany,返回值也是 any,因为实际运行时可能返回 stringnumber。函数体中的 a + b 根据 JavaScript 的 + 规则:字符串拼接或数字相加。

6.2.3. 实际调用与类型检查

typescript 复制代码
combine("Hello ", "World"); // 返回 string,类型推断为 string
combine(1, 2);              // 返回 number,类型推断为 number

这两次调用分别匹配第一个和第二个重载签名。TypeScript 根据传入参数的类型,选择对应的重载签名,并知道返回值的精确类型。

6.2.4. 错误调用

typescript 复制代码
// combine("a", 2); // ❌ 报错:没有匹配的重载

为什么报错?

因为 combine("a", 2) 的参数是 stringnumber,既不符合 (string, string),也不符合 (number, number)。TypeScript 在重载签名列表中逐个检查,找不到匹配项,于是抛出错误。即使实现签名用了 any,实现签名也不作为直接调用的类型签名,它只用来检查函数体是否兼容所有重载签名。


6.3、重载的规则和原理

  1. 重载签名写在实现签名前面,而且之间不能有函数体。
  2. 实现签名不可见给外部调用者。外部调用时只能看到重载签名列表。
  3. 实现签名的参数与返回类型必须兼容所有重载签名 。通常使用联合类型或 any 来涵盖。
  4. 匹配时按顺序检查,第一个匹配的签名胜出(所以更具体的签名放在前面)。

6.4、生活类比

想象一个"榨汁机":

  • 如果你放入苹果,它给你苹果汁。
  • 如果你放入橙子,它给你橙汁。
  • 你不能放入石头,因为榨汁机不接受石头。

重载签名就是这台榨汁机的使用说明书,上面写着:可以放苹果(返回苹果汁),可以放橙子(返回橙汁)。实现则是机器内部的刀片,管你放什么进去,它都会切,但说明书(类型系统)限制了你只能放苹果或橙子。


6.5、更多重载示例

例1:根据参数类型返回不同结果

typescript 复制代码
function getInfo(id: number): { id: number; name: string };
function getInfo(name: string): { id: number; name: string }[];
function getInfo(param: number | string): any {
  if (typeof param === "number") {
    return { id: param, name: "Alice" };
  } else {
    return [{ id: 1, name: param }];
  }
}

getInfo(1);      // 返回 { id: 1, name: 'Alice' }
getInfo("Bob");  // 返回 [{ id: 1, name: 'Bob' }]

例2:参数个数不同

typescript 复制代码
function print(value: string): void;
function print(value: string, times: number): void;
function print(value: string, times?: number): void {
  const repeat = times ?? 1;
  for (let i = 0; i < repeat; i++) {
    console.log(value);
  }
}

print("Hello");       // 输出一次
print("World", 3);    // 输出三次

七、泛型(Generics)------ 写出灵活又安全的代码

概念

泛型就像是"类型变量",在定义函数、接口或类时不预先指定具体类型,而在使用的时候再确定。

生活类比:一个空盒子,你可以放进苹果,也可以放进书。盒子本身不限制内容,但当你放进去之后,它就确定了内容类型。

为什么需要泛型?

  • 保持代码复用性:同一个函数可以处理不同类型的数据。
  • 保证类型安全:使用泛型后,传入和返回的类型保持一致,不会丢失类型信息。

基础用法

typescript 复制代码
function identity<T>(arg: T): T {
  return arg;
}

// 调用方式1:显式指定类型
let output1 = identity<string>("hello");
// 调用方式2:类型推断
let output2 = identity(123); // output2 为 number 类型

这里 <T> 就是泛型变量,可以理解为"类型的占位符"。identity 函数接收任意类型 T,并返回同类型值。

泛型约束

有些时候,你想限制泛型参数必须具有某些属性,比如有 length 属性。使用 extends 关键字:

typescript 复制代码
interface Lengthwise {
  length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}
logLength("abc"); // OK, string 有 length
logLength([1,2]); // OK
logLength(123); // 报错,number 没有 length

原理T extends Lengthwise 告诉编译器,T 必须满足 Lengthwise 类型(具有 length: number)。这样函数体内就可以安全访问 arg.length

泛型接口与泛型类

typescript 复制代码
interface Result<T> {
  data: T;
  success: boolean;
}
const res: Result<string> = { data: "ok", success: true };

class Stack<T> {
  private items: T[] = [];
  push(item: T) { this.items.push(item); }
  pop(): T | undefined { return this.items.pop(); }
}
const numberStack = new Stack<number>();
numberStack.push(10);

注意:泛型在类中为实例方法提供类型安全,静态成员不能使用类的泛型。


八、高级类型:构建类型的强大工具

8.1、索引类型查询 keyof

概念

keyof T 取出类型 T 的所有键名 ,生成一个联合类型

为什么需要?

当你想限制某个变量只能是对象的合法键名时,而不是任意的 string,就可以用 keyof

示例

typescript 复制代码
interface Person {
  name: string;
  age: number;
}

type PersonKeys = keyof Person; // "name" | "age"

使用场景

typescript 复制代码
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const person: Person = { name: "Alice", age: 25 };
const n = getProperty(person, "name"); // n 是 string
 getProperty(person, "email"); // ❌ 报错,email 不是 keyof Person

原理与注意

  • keyof 对对象类型返回其所有公共属性名的联合。
  • 对联合类型,keyof 返回所有联合成员共有的属性名(交集)。
  • anykeyof anystring | number | symbol

8.2、索引访问类型 T[K]

概念

像访问对象属性一样访问类型,T[K] 得到类型 T 中属性 K 的类型。

示例

typescript 复制代码
type PersonName = Person["name"]; // string
type PersonAge = Person["age"];   // number

结合 keyof

typescript 复制代码
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

这里 T[K] 表示:返回值类型就是对象中该属性对应的类型。调用 getProperty(person, "name") 返回类型自动为 string

注意

  • K 必须满足 extends keyof T,否则报错。
  • 可以通过联合类型一次性获取多种属性的类型:Person["name" | "age"] 得到 string | number

8.3、映射类型(Mapped Types)

概念

基于已有类型的键,通过遍历键来生成一个新类型,并可以给每个属性加上修饰符(readonly?)或改变值的类型。

为什么需要?

比如你有一个 Todo 接口,想生成一个所有字段都可选的版本,或者所有字段只读的版本。手工重复定义非常繁琐且容易遗漏。

基础语法

typescript 复制代码
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};
type Partial<T> = {
  [K in keyof T]?: T[K];
};
  • [K in keyof T]:遍历 T 的所有键,K 在每次迭代中是一个键名字面量。
  • 右侧 T[K]:取出该键对应的类型。

示例

typescript 复制代码
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type ReadonlyTodo = Readonly<Todo>;
// { readonly title: string; readonly description: string; readonly completed: boolean; }

type PartialTodo = Partial<Todo>;
// { title?: string; description?: string; completed?: boolean; }

更进一步:带条件的映射

你可以使用 as 子句对键进行重新映射(TypeScript 4.1+):

typescript 复制代码
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface User {
  name: string;
  age: number;
}
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number; }

这里 as 重命名了键,Capitalize 是内置工具类型,将首字母大写。

原理与注意

  • 映射类型生成新的对象类型,不会影响原类型。
  • 可以组合多个修饰符,例如 -readonly 移除只读,-? 移除可选。
  • 遍历的是 keyof T,所以只处理对象类型的属性。

8.4、条件类型(Conditional Types)

概念

根据某个类型是否满足条件,返回不同的类型。语法类似于三元表达式:T extends U ? X : Y

为什么需要?

很多工具类型需要根据输入类型的不同输出不同结果,比如提取函数返回类型、排除 null 等。

基础示例

typescript 复制代码
type IsString<T> = T extends string ? true : false;
type A = IsString<"abc">; // true
type B = IsString<42>;    // false

分布式条件类型

当条件类型的左侧是裸类型参数 (直接写成 T extends ...)且 T 是联合类型时,条件类型会自动分布到联合类型的每个成员上。

typescript 复制代码
type ToArray<T> = T extends any ? T[] : never;
type StrOrNumArray = ToArray<string | number>; // string[] | number[]
// 而不是 (string | number)[]

这个特性是实现 ExcludeExtract 等工具类型的基础。

注意

  • 要避免分布,可以用元组包裹:[T] extends [U] ? ...
  • 条件类型中可以使用 infer 进行类型推断。

8.5、infer 关键字

概念

infer 只能在条件类型的 extends 子句中使用,用于声明一个待推断的类型变量。

为什么需要?

当你需要从一个复杂类型中提取出某个部分时(比如函数返回值类型、Promise 包裹的类型),infer 可以帮你"解包"。

示例:提取函数返回类型

typescript 复制代码
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Fn = (x: number) => string;
type R = ReturnType<Fn>; // string

解释:

  • T extends (...args: any[]) => infer R:如果 T 是一个函数类型,那么推断出它的返回值类型并赋值给 R
  • 条件为真时返回 R,否则返回 never

更多应用

  • 提取数组元素类型:T extends (infer U)[] ? U : never
  • 提取 Promise 结果类型:T extends Promise<infer V> ? V : never
  • 提取构造函数实例类型:T extends new (...args: any[]) => infer I ? I : never

原理与注意

  • infer 只能在 extendstrue 分支中使用。
  • 同一个条件类型可以同时有多个 infer,例如提取函数参数类型:T extends (...args: infer P) => any ? P : never

8.6、常用的内置工具类型

TypeScript 内置了许多基于上述特性实现的工具类型,掌握它们可以大幅提升开发效率。

1. Partial<T> ------ 所有属性变为可选

作用 :把类型 T 的所有属性都变成可选的(加上 ?)。

为什么需要:当你需要更新一个对象的某些字段时,可以传递部分属性而不必提供全部。

示例

typescript 复制代码
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type PartialTodo = Partial<Todo>;

输出结果类型

typescript 复制代码
{
  title?: string;
  description?: string;
  completed?: boolean;
}

使用效果

typescript 复制代码
const todo1: PartialTodo = { title: "Learn TS" }; // ✅ 只传 title
const todo2: PartialTodo = {};                    // ✅ 空对象也行
 const todo3: Todo = { title: "Learn TS" };     // ❌ 缺少其他属性

解释Partial 内部遍历 T 的所有键,给每个属性前加上 ?,相当于把"必须填"变成了"选填"。


2. Required<T> ------ 所有属性变为必选

作用 :把类型 T 的所有属性都变成必填的(去掉 ?)。

为什么需要:有时从一个可选类型出发,需要生成一个所有字段都强制存在的类型。

示例

typescript 复制代码
interface Config {
  host?: string;
  port?: number;
  debug?: boolean;
}

type RequiredConfig = Required<Config>;

输出结果类型

typescript 复制代码
{
  host: string;
  port: number;
  debug: boolean;
}

使用效果

typescript 复制代码
const cfg: RequiredConfig = {
  host: "localhost",
  port: 8080,
  debug: false
}; // ✅ 必须三个全给
 const cfg2: RequiredConfig = { host: "localhost" }; // ❌ 缺少 port 和 debug

解释Required 内部使用了 -? 修饰符,意思是"移除可选",强制每个属性都必填。


3. Readonly<T> ------ 所有属性变为只读

作用 :把类型 T 的所有属性都变成只读的(加上 readonly)。

为什么需要:保证对象一旦创建就不能被修改,常用于配置对象或状态快照。

示例

typescript 复制代码
interface User {
  name: string;
  age: number;
}

type ReadonlyUser = Readonly<User>;

输出结果类型

typescript 复制代码
{
  readonly name: string;
  readonly age: number;
}

使用效果

typescript 复制代码
const user: ReadonlyUser = { name: "Alice", age: 25 };
user.name = "Bob"; // ❌ 报错:无法分配到 "name",因为它是只读属性

解释Readonly 内部遍历所有键,给每个属性前加上 readonly,防止修改。


4. Pick<T, K> ------ 从 T 中挑选指定属性

作用 :从类型 T 中只挑选出 K 中指定的属性,构造一个新类型。

为什么需要:当你只需要一个对象中的少数几个字段时,比如展示预览信息。

示例

typescript 复制代码
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

输出结果类型

typescript 复制代码
{
  title: string;
  completed: boolean;
}

使用效果

typescript 复制代码
const preview: TodoPreview = {
  title: "Learn TS",
  completed: false
}; // ✅ 只含两个属性
const preview2: TodoPreview = { title: "TS" }; // ❌ 缺少 completed

解释Pick 第一个参数是源类型,第二个参数是要保留的键的联合类型。内部遍历 K,取 T[K] 作为值类型。


5. Omit<T, K> ------ 从 T 中删除指定属性

作用 :从类型 T 中剔除 K 中指定的属性,返回剩下的。

为什么需要 :与 Pick 相反,当你知道不需要哪些字段时,用 Omit 更直观。

示例

typescript 复制代码
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoWithoutDesc = Omit<Todo, "description">;

输出结果类型

typescript 复制代码
{
  title: string;
  completed: boolean;
}

使用效果

typescript 复制代码
const todo: TodoWithoutDesc = {
  title: "Learn TS",
  completed: true
}; // ✅ 不用写 description
const todo2: TodoWithoutDesc = { title: "TS" }; // ❌ 缺少 completed

解释Omit 内部先通过 Exclude<keyof T, K> 得到剩余的键,再配合 Pick 生成新类型。本质上相当于 Pick<T, Exclude<keyof T, K>>


6. Record<K, T> ------ 构造对象类型

作用 :创建一个对象类型,它的键都是 K 类型,值都是 T 类型。

为什么需要:快速定义字典、映射表、配置集等场景。

示例

typescript 复制代码
type Page = "home" | "about" | "contact";
type PageInfo = Record<Page, { title: string }>;

输出结果类型

typescript 复制代码
{
  home: { title: string };
  about: { title: string };
  contact: { title: string };
}

使用效果

typescript 复制代码
const pages: PageInfo = {
  home: { title: "Home" },
  about: { title: "About" },
  contact: { title: "Contact" }
}; // ✅ 键必须全部包含
const pages2: PageInfo = { home: { title: "A" } }; // ❌ 缺少 about 和 contact

解释Record 第一个参数 K 必须能赋值给 string | number | symbol,第二个参数 T 是值的类型。内部遍历 K 的每个成员,构造一个属性,所有值类型都是 T


7. Exclude<T, U> ------ 从联合类型中排除

作用 :从联合类型 T 中剔除所有能赋值给 U 的成员。

为什么需要:去除联合类型中的某些特定类型。

示例

typescript 复制代码
type Mixed = string | number | boolean;
type OnlyPrimitive = Exclude<Mixed, boolean>;

输出结果类型

typescript 复制代码
string | number

使用效果

typescript 复制代码
let x: OnlyPrimitive;
x = "hello"; // ✅
x = 42;      // ✅
x = true; // ❌ boolean 已被排除

解释Exclude 利用了条件类型的分布式 特性,对 T 的每个成员执行 T extends U ? never : T,如果该成员能赋值给 U,就返回 never(从结果中消失),否则保留。


8. Extract<T, U> ------ 从联合类型中提取

作用 :从联合类型 T 中提取出所有能赋值给 U 的成员。

为什么需要 :与 Exclude 相反,获取交集部分。

示例

typescript 复制代码
type Mixed = string | number | boolean;
type StringOrBool = Extract<Mixed, string | boolean>;

输出结果类型

typescript 复制代码
string | boolean

使用效果

typescript 复制代码
let y: StringOrBool;
y = "abc";  // ✅
y = false;  // ✅
// y = 10;  // ❌ number 不在提取范围内

解释 :内部实现是 T extends U ? T : never,只保留能赋值给 U 的成员。


9. NonNullable<T> ------ 排除 nullundefined

作用 :从类型 T 中剔除 nullundefined

为什么需要:确保一个值不为空,常用于严格模式下。

示例

typescript 复制代码
type MaybeString = string | null | undefined;
type SureString = NonNullable<MaybeString>;

输出结果类型

typescript 复制代码
string

使用效果

typescript 复制代码
let s: SureString;
s = "hello"; // ✅
s = null;    // ❌
s = undefined; // ❌

解释 :底层实现 T extends null | undefined ? never : T,把空值过滤掉。也可以写成 T & {} 达到相似效果。


10. ReturnType<T> ------ 获取函数返回值类型

作用:提取一个函数类型的返回值类型。

为什么需要:当你需要复用某个函数的返回值类型,或者做依赖推断时。

示例

typescript 复制代码
function createUser(name: string, age: number) {
  return { name, age, id: Math.random() };
}

type UserReturn = ReturnType<typeof createUser>;

输出结果类型

typescript 复制代码
{
  name: string;
  age: number;
  id: number;
}

使用效果

typescript 复制代码
const user: UserReturn = {
  name: "Alice",
  age: 25,
  id: 0.12345
}; // ✅ 结构与函数返回值一致
 const user2: UserReturn = { name: "Bob" }; // ❌ 缺少 age 和 id

解释ReturnType 内部用 infer 推断返回值类型。typeof createUser 获取函数类型,然后 extends (...args: any[]) => infer R ? R : any 提取 R


11. Parameters<T> ------ 获取函数参数类型

作用:提取一个函数类型的参数列表类型,以元组形式返回。

为什么需要:需要复用某个函数的参数列表,或做参数转发。

示例

typescript 复制代码
function createUser(name: string, age: number) {
  return { name, age };
}

type CreateUserParams = Parameters<typeof createUser>;

输出结果类型

typescript 复制代码
[name: string, age: number]

使用效果

typescript 复制代码
const args: CreateUserParams = ["Alice", 25]; // ✅ 按参数顺序和类型提供
const args2: CreateUserParams = [123, "Bob"]; // ❌ 类型顺序不对

解释 :内部用 infer 提取:T extends (...args: infer P) => any ? P : never


12. Awaited<T> ------ 递归解包 Promise

作用 :获取 Promise 最终 resolve 的类型,支持嵌套 Promise

为什么需要:异步函数经常包装多层 Promise,想获得最终结果类型。

示例

typescript 复制代码
type X = Promise<string>;
type Y = Promise<Promise<number>>;
type Z = Promise<Promise<Promise<boolean>>>;

type AX = Awaited<X>; // string
type AY = Awaited<Y>; // number
type AZ = Awaited<Z>; // boolean

输出结果类型

typescript 复制代码
string, number, boolean

使用效果

typescript 复制代码
async function fetchData(): Promise<string> {
  return "data";
}
type Data = Awaited<ReturnType<typeof fetchData>>; // string
// 直接得到 string,而不是 Promise<string>

解释Awaited 内部递归:T extends Promise<infer V> ? Awaited<V> : T,直到剥到非 Promise 类型为止。


综合应用举例

实际项目中,常常组合使用这些工具类型:

typescript 复制代码
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

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

// 返回给前端的用户信息,去掉 password
type PublicUser = Omit<User, "password">;

// 前端请求到的响应体数据类型
type UserResponse = ApiResponse<PublicUser>;
// 结果:{ data: { id: number; name: string; email: string }; status: number; message: string; }

// 更新用户时只传部分字段
type UpdateUserPayload = Partial<Pick<User, "name" | "email">>;
// 结果:{ name?: string; email?: string; }

8.7、高级类型组合实例:DeepReadonly

利用映射类型、条件类型和递归,实现深层只读:

typescript 复制代码
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepReadonly<T[K]>
    : T[K];
};

interface Config {
  db: {
    host: string;
    port: number;
  };
  debug: boolean;
}

type ReadonlyConfig = DeepReadonly<Config>;
// { readonly db: { readonly host: string; readonly port: number }; readonly debug: boolean; }

这个例子综合了 keyof、映射类型、条件类型和递归,足以体现高级类型的强大。


九、类(Class)在 TypeScript 中的增强

9.1、类的基本写法(类型注解)

TypeScript 允许在类中为属性添加类型注解,构造函数参数也可以标注类型。

typescript 复制代码
class Point {
  x: number;
  y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

const p = new Point(10, 20);
p.x = 30; // ✅
p.x = "hello"; // ❌ 类型错误

解释 :属性 xy 的类型都是 number,任何赋值都会检查类型。如果没有显式声明属性,直接在 constructorthis.x = x 会报错,因为 TS 要求先声明属性。


9.2、访问修饰符 ------ 控制属性和方法的可见性

访问修饰符决定了属性或方法可以在哪里被访问。TypeScript 提供了三个修饰符:

  • public(默认):任何地方都能访问。
  • protected:当前类和子类能访问,类外部不能访问。
  • private:只有当前类自己能访问,子类和外部都不能访问。

9.2.1. public 显式或默认

typescript 复制代码
class Animal {
  public name: string;   // 等价于不写修饰符

  constructor(name: string) {
    this.name = name;
  }
}

const cat = new Animal("Kitty");
console.log(cat.name); // ✅ 外部可读
cat.name = "Tom";      // ✅ 外部可写

解释 :不加修饰符就是 public,外部可随意访问。

9.2.2. protected ------ 允许子类访问

typescript 复制代码
class Animal {
  protected age: number;

  constructor(age: number) {
    this.age = age;
  }

  protected getAge(): number {
     // ✅ 内部可以访问 protected 属性
    return this.age;
  }
}

class Dog extends Animal {
  constructor(age: number) {
    super(age);
  }

  public showAge() {
    console.log(this.age);    // ✅ 子类可以访问 protected 属性
    console.log(this.getAge()); // ✅ 子类可以调用 protected 方法
  }
}

const dog = new Dog(3);
dog.showAge(); // 3
console.log(dog.age);      // ❌ 错误:属性"age"是受保护的,只能在类及子类中访问
dog.getAge();              // ❌ 错误

解释agegetAge() 都是 protected,只能在 Animal 内部及其子类 Dog 中访问,实例 dog 外部不能访问。这样的封装可以对外隐藏内部细节,又能让派生类继承。

9.2.3. private ------ 只有自身能访问

typescript 复制代码
class Animal {
  private secret: string;

  constructor(secret: string) {
    this.secret = secret;
  }

  public reveal() {   
    return this.secret; // ✅ 类内部可以访问
  }
}

class Cat extends Animal {
  constructor() {
    super("cat secret");
  }

  public tryReveal() {
    return this.secret; // ❌ 错误:属性"secret"为私有属性,只能在类"Animal"中访问
  }
}

const animal = new Animal("my secret");
console.log(animal.reveal()); // ✅
console.log(animal.secret); // ❌ 错误

解释private 只有当前类内部能访问,子类也不行。这用来隐藏真正的内部实现。

9.2.4. readonly ------ 只读属性

typescript 复制代码
class Person {
  readonly id: number;
  public name: string;

  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }
}

const p = new Person(1, "Alice");
console.log(p.id); // ✅
 p.id = 2;       // ❌ 错误:无法分配到 "id",因为它是只读属性
p.name = "Bob";    // ✅ 非只读属性可以修改

解释readonly 属性只能在声明时或构造函数中赋值,之后不能修改。适合用作 ID、创建时间等不可变数据。

9.2.5. 参数属性 ------ 简写形式

在构造函数参数前直接加上访问修饰符或 readonly,可以同时声明属性和赋值。

typescript 复制代码
class Employee {
  constructor(
    public name: string,
    private salary: number,
    readonly id: number
  ) {
    // 不需要手动赋值,TS 会自动完成
  }
}

const emp = new Employee("Bob", 5000, 101);
console.log(emp.name); // ✅
console.log(emp.salary); // ❌ 私有属性
 emp.id = 102;            // ❌ 只读

解释public name 相当于在类中声明了 public name: string,然后在构造函数中自动把参数值赋给 this.name。这大大减少了样板代码。


9.3、抽象类 ------ 不能被实例化的类

概念与为什么需要

当你有一个基类,它本身不应该被直接 new 出来,而应该由子类去实现具体逻辑时,就用抽象类。抽象类只描述"应该有什么",不负责具体实现。

  • abstract 关键字标记类或方法。
  • 抽象方法只有签名,没有方法体,子类必须实现。
  • 可以包含具体实现的方法(非抽象方法)。

示例

typescript 复制代码
abstract class Shape {
  abstract getArea(): number; // 抽象方法:子类必须实现

  // 具体方法:子类可以直接用
  describe(): void {
    console.log("This shape's area is " + this.getArea());
  }
}

class Circle extends Shape {
  constructor(public radius: number) {
    super(); // 必须调用基类构造函数
  }

  // 实现抽象方法
  getArea(): number {
    return Math.PI * this.radius ** 2;
  }
}

 const shape = new Shape(); // ❌ 错误:不能创建抽象类的实例
const circle = new Circle(10);
circle.describe(); // This shape's area is 314.159...
console.log(circle.getArea()); // 314.159...

正确与错误

typescript 复制代码
// ✅ 子类继承抽象类,实现了 getArea
class Rectangle extends Shape {
  constructor(public width: number, public height: number) {
    super();
  }
  getArea(): number {
    return this.width * this.height;
  }
}

// ❌ 没有实现抽象方法 getArea,会报错
class Square extends Shape {
  constructor(public side: number) {
    super();
  }
   // 缺少 getArea
}

解释:抽象类强制子类遵循约定的接口,让设计更清晰。


9.4、类实现接口 (implements)

概念与为什么需要

有时你想让一个类必须 包含某些属性和方法,但不需要从某个父类继承实现。这时可以用接口 定义契约,用 implements 来保证类符合接口。

示例

typescript 复制代码
interface Printable {
  content: string;
  print(): void;
}

class Document implements Printable {
  content: string;

  constructor(content: string) {
    this.content = content;
  }

  print(): void {
    console.log(this.content);
  }
}

const doc = new Document("Hello TypeScript");
doc.print(); // Hello TypeScript

错误示例

typescript 复制代码
// ❌ 缺少 content 属性或 print 方法
 class Report implements Printable {
  print() { console.log("empty"); }
 } 
// 报错:类"Report"缺少属性"content"

解释implements 只在类型层面进行检查,编译后不产生任何代码。一个类可以实现多个接口,用逗号分隔。


9.5、类既是值又是类型

在 TypeScript 中,class 声明既创建了运行时值 (构造函数),也创建了类型(实例的类型)。

typescript 复制代码
class Cat {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

// 作为类型使用
let cat: Cat;               // Cat 是实例的类型
cat = new Cat("Kitty");     // ✅
cat = { name: "Tom" };     // ✅ 结构类型兼容

// 作为值使用(构造函数本身)
const CatCtor: typeof Cat = Cat;
const anotherCat = new CatCtor("Jerry");

解释 :当你写 let c: Cat 时,Cat 是实例的类型;当你写 typeof Cat 时,拿到的是构造函数本身的类型。这种双重性质在依赖注入、工厂函数中很有用。


9.6、重要注意事项

9.6.1. TypeScript 的 privateprotected 只在编译时检查

编译成 JavaScript 后,这些修饰符会被移除,属性在运行时仍然是可访问的。

typescript 复制代码
class Secret {
  private data = "hidden";
}
const s = new Secret(); 
(s as any).data; // 运行时可访问,但 TS 编译器会报错

真正的运行时私有 :使用 ECMAScript 的 # 私有字段(硬性私有)。

typescript 复制代码
class BankAccount {
  #balance = 0;  // JavaScript 原生私有字段

  getBalance() {
    return this.#balance;
  }
}

const account = new BankAccount();
account.#balance; // ❌ 语法错误(运行时也是私有的)

9.6.2. 构造函数参数用 public 简写时,注意与属性同名覆盖

typescript 复制代码
class Foo {
  constructor(public x: number) {}
}
// 等价于:
class Foo {
  x: number;
  constructor(x: number) {
    this.x = x;
  }
}

9.6.3. 抽象类可以被继承但不能 new,而接口只能被 implements 或继承,不能实例化。


9.7、总结

特性 作用 关键字
访问修饰符 控制可见性 public, protected, private
只读属性 禁止修改 readonly
参数属性 声明并赋值一步到位 构造函数的修饰符参数
抽象类 强制子类实现特定方法 abstract class
抽象方法 无实现,由子类提供 abstract method()
接口实现 保证类满足接口契约 implements

十、模块与命名空间

现代项目使用 ES Modules(import / export)。

typescript 复制代码
// point.ts
export interface Point { x: number; y: number; }
export function draw(p: Point) { ... }

// app.ts
import { Point, draw } from "./point";

namespace 主要是早期用于组织全局代码的,现在已被 ES Modules 取代。了解即可,不推荐新项目使用。


十一、类型声明文件(.d.ts)

当你在 TypeScript 项目中使用 JavaScript 库时,TS 无法得知库的类型,需要声明文件来描述类型。

  • 社区大多已提供:npm install @types/库名
  • 如果没有,可自行编写 xxx.d.ts
typescript 复制代码
// global.d.ts
declare module "*.css" {
  const content: { [className: string]: string };
  export default content;
}

// 声明全局变量或方法
declare global {
  interface Window {
    myApp: any;
  }
}

注意:声明文件只用于类型检查,不包含实现。


十二、tsconfig.json 关键配置

tsconfig.json 是 TypeScript 项目的配置文件,控制编译行为。

json 复制代码
{
  "compilerOptions": {
    "target": "ES2020",       // 编译后的 JS 版本
    "module": "ESNext",       // 模块系统
    "lib": ["ES2020", "DOM"], // 使用的内置 API 声明
    "strict": true,           // 启用所有严格类型检查
    "esModuleInterop": true,  // 兼容 CommonJS 导入
    "skipLibCheck": true,     // 跳过声明文件检查,加速编译
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "react-jsx"
  },
  "include": ["src"]
}

最重要的选项是 strict: true,它一次性开启以下严格检查:

  • noImplicitAny:禁止隐式 any
  • strictNullChecks:严格空值检查
  • strictFunctionTypes:严格函数类型检查
  • 等等

初学者务必开启 strict,虽然一开始会多出很多报错,但这能帮你养成类型安全的习惯。


十三、面试常考实战题型

1. interface vs type(见第三节)

2. any vs unknown

unknown 是类型安全的顶类型,使用前必须检查类型。面试中经常要求说出两者区别并举例。

3. 泛型约束的应用

例如要求泛型参数必须具有某属性,或者根据泛型返回不同类型。

4. 提取数组元素类型

typescript 复制代码
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type El = ArrayElement<string[]>; // string

5. 让对象所有属性可选 / 必选 / 只读

使用 Partial<T>, Required<T>, Readonly<T>

6. as const 断言

将对象或数组变为字面量只读类型。

typescript 复制代码
const colors = ["red", "green"] as const;
// 类型:readonly ["red", "green"]

作用:获得更精确的类型,且数据不可修改。

7. 双重断言

typescript 复制代码
const el = document.getElementById("app") as unknown as HTMLCanvasElement;

不推荐,除非万不得已,一般先用类型守卫。

8. 编写自定义类型守卫

typescript 复制代码
function isString(value: unknown): value is string {
  return typeof value === "string";
}
相关推荐
threerocks1 小时前
什么?我连 A2A、MCP 都没学会,现在又来了 AG-UI、A2UI.
前端·aigc·ai编程
牛奶2 小时前
如何自己写一个浏览器插件?
前端·chrome·浏览器
亿元程序员3 小时前
为什么Cocos都4.0了还有人用2.x?
前端
MomentYY3 小时前
AI 到底是“懂”,还是在“猜”?
前端·人工智能·ai编程
鹏毓网络科技3 小时前
Cursor Rules 文件配置实战:3 个隐藏参数让我每月少写 40% 样板代码
前端·github
没烦恼3013 小时前
无痕模式下 HTTP\-First 拦截引发的“页面刷新”误判
前端
文心快码BaiduComate3 小时前
从个人提效到组织提效:Comate辅助构建自我进化的AI研发系统
前端·程序员
未秃头的程序猿3 小时前
Java 26正式发布!这3个新特性,让代码量直接减半
java·后端·面试
hunterandroid3 小时前
Compose 状态管理:remember、rememberSaveable 与状态提升
前端
星栈4 小时前
Dioxus 接数据库最容易写歪的 3 个地方:sqlx + SQLite 怎么接才顺
前端·rust·前端框架