三列布局与 TypeScript 工具类型 Pick / Omit / Partial 详解

文章目录

    • [一、CSS 三列布局](#一、CSS 三列布局)
      • [1.1 什么是三列布局](#1.1 什么是三列布局)
      • [1.2 前置知识:格式化上下文(BFC / FFC / GFC)](#1.2 前置知识:格式化上下文(BFC / FFC / GFC))
      • [1.3 方案一:Flex 弹性布局(最常用)](#1.3 方案一:Flex 弹性布局(最常用))
      • [1.4 方案二:Grid 网格布局(最简洁)](#1.4 方案二:Grid 网格布局(最简洁))
      • [1.5 方案三:Float 浮动布局(最古老,需理解 BFC)](#1.5 方案三:Float 浮动布局(最古老,需理解 BFC))
      • [1.6 三种方案对比](#1.6 三种方案对比)
    • [二、TypeScript 工具类型](#二、TypeScript 工具类型)
      • [2.1 为什么需要工具类型](#2.1 为什么需要工具类型)
      • [2.2 基础:keyof 与 typeof](#2.2 基础:keyof 与 typeof)
      • [2.3 Pick:挑选字段](#2.3 Pick:挑选字段)
      • [2.4 Omit:剔除字段](#2.4 Omit:剔除字段)
      • [2.5 重点:Omit = Pick + Exclude](#2.5 重点:Omit = Pick + Exclude)
      • [2.6 Partial:全字段可选](#2.6 Partial:全字段可选)
      • [2.7 Record 与 ReturnType](#2.7 Record 与 ReturnType)
      • [2.8 Exclude 与 Omit 的区别](#2.8 Exclude 与 Omit 的区别)
    • 三、全文总结
    • 四、核心知识点复盘
    • [五、常见问题 / 避坑指南](#五、常见问题 / 避坑指南)

这是一篇面向「基础偏薄弱但想系统搞懂」的前端同学的文章。前半部分用三种方式(Flex / Grid / Float)讲透 PC 端最常见的三列布局 ,以及它背后的核心概念------格式化上下文(BFC / FFC / GFC) ;后半部分讲 TypeScript 内置工具类型 (Pick、Omit、Partial、Record、ReturnType),重点拆解 Omit = Pick + Exclude 这条底层等价关系。每个知识点都按「是什么 → 为什么 → 怎么用 → 易错点」的顺序展开,代码完整可运行。


一、CSS 三列布局

1.1 什么是三列布局

在 PC 端网页里,有一种非常经典的页面骨架:左右两列固定宽度,中间一列自适应撑满剩余空间。典型场景就是下面这种结构:

复制代码
┌────────────┬──────────────────────────┬────────────┐
│  左侧导航    │      中间主内容           │  右侧广告    │
│  (固定 200px)│      (自适应剩余宽度)      │  (固定 200px)│
└────────────┴──────────────────────────┴────────────┘

它的 HTML 骨架长这样:

html 复制代码
<div class="layout">
  <aside class="sidebar left">左侧导航</aside>
  <main class="content">中间主内容(自适应)</main>
  <aside class="sidebar right">右侧广告</aside>
</div>

这里有一个很关键的细节 :为什么要把 <main>(主内容)放在中间、优先渲染?因为两侧通常是导航、广告这类「次要信息」,而主内容才是用户最想第一时间看到的东西。让主内容先渲染,页面感知速度更快,SEO 也更友好。这个「内容优先」的诉求,决定了我们后面选择布局方案时的取舍。

要真正理解三列布局,得先搞懂一个底层概念------格式化上下文

1.2 前置知识:格式化上下文(BFC / FFC / GFC)

这部分是 CSS 布局的地基,也是很多同学「知其然不知其所以然」的地方,我们把它讲透。

文档流(Normal Flow) 是浏览器默认的排版规则:

  • 块级元素divph1...)从上到下垂直排列;
  • 行内元素spanaem...)从左到右水平排列。

<html> 作为根元素,一启动就开启了第一个格式化上下文(BFC),所有元素默认都排在这个「最外层的大容器」里。

BFC(Block Formatting Context,块级格式化上下文) 可以理解为一个「独立的排版作用域」:在这个作用域里,元素按照自己的规则排列,外层的 BFC 不会影响到内部新的格式化上下文,内部也不会反过来干扰外部。

问题来了:默认的 BFC 只能让块级元素从上往下排,没法做「左右分列」这种局部复杂布局 。而 inline 行内元素又天生不适合当容器盒子。所以要实现三列布局,就必须「开启一个新的格式化上下文」,让某个块级元素拥有独立的、更强大的排版能力。

开启新的格式化上下文的常见方式:

写法 开启的上下文 说明
display: flex FFC(Flex 格式化上下文) 一维布局,主轴方向排列
display: grid GFC(Grid 格式化上下文) 二维布局,行列同时控制
display: table / inline-block 新的 BFC 表格 / 行内块排版
float: left / right 新的 BFC 元素脱离文档流浮动
position: absolute / fixed 新的 BFC 脱离文档流定位
overflow: hidden / auto / scroll 新的 BFC 触发「块级格式化上下文」

记住一句话:只要让一个块级元素开启新的格式化上下文,它内部就能独立排版,不再受外层默认文档流的束缚。三列布局正是靠这个实现的。

下面用三种方式分别实现,感受它们各自的特点。

1.3 方案一:Flex 弹性布局(最常用)

Flex 是一维布局:容器设置 display: flex 后,子项会沿着「主轴」排列。中间列用 flex: 1 吃掉所有剩余空间即可。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>三列布局 - Flex</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }

    .layout {
      display: flex;      /* 开启弹性格式化上下文 FFC */
      height: 100vh;      /* 占满视口高度,方便观察 */
    }

    /* 左右两列:固定宽度,且不允许被压缩 */
    .sidebar {
      width: 200px;
      flex-shrink: 0;     /* 关键:默认 flex 子项可能被压缩,这里禁止压缩 */
    }
    .left  { background: #f0c0c0; }
    .right { background: #c0c0f0; }

    /* 中间主内容:自适应剩余宽度 */
    .content {
      flex: 1;            /* 等价于 flex: 1 1 0%,撑满剩余空间 */
      background: #fff;
    }
  </style>
</head>
<body>
  <div class="layout">
    <aside class="sidebar left">左侧导航</aside>
    <main class="content">中间主内容(自适应宽度)</main>
    <aside class="sidebar right">右侧广告</aside>
  </div>
</body>
</html>

代码思路解析:

  1. .layoutdisplay: flex,把三个子项排成一行;
  2. 两侧 .sidebar 固定 width: 200px,并加 flex-shrink: 0------因为 flex 子项默认允许被「压缩」,不加这个,当内容多时两侧会被挤窄,固定宽度就失效了;
  3. 中间 .contentflex: 1,表示「占据所有剩余空间」,宽度自动适配。

易错点: 很多同学只写 width: 200px 却忘了 flex-shrink: 0,结果窗口变窄时左右两列被压变形。这是 flex 布局里最隐蔽的坑。

1.4 方案二:Grid 网格布局(最简洁)

Grid 是二维布局,可以直接用 grid-template-columns 一次声明三列的宽度。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>三列布局 - Grid</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }

    .layout {
      display: grid;
      /* 三列:左 200px | 中间 1fr 自适应 | 右 200px */
      grid-template-columns: 200px 1fr 200px;
      height: 100vh;
    }

    .left  { background: #f0c0c0; }
    .right { background: #c0c0f0; }
    .content { background: #fff; }
  </style>
</head>
<body>
  <div class="layout">
    <aside class="sidebar left">左侧导航</aside>
    <main class="content">中间主内容(自适应)</main>
    <aside class="sidebar right">右侧广告</aside>
  </div>
</body>
</html>

代码思路解析:

  1. display: grid 开启网格格式化上下文 GFC;
  2. grid-template-columns: 200px 1fr 200px 里,200px 是固定列宽,1fr 是「按剩余空间等分 1 份」,于是中间列自动撑满。

fr(fraction,份数)是 Grid 独有的单位,非常好用。相比之下,Grid 写三列布局代码最少、语义最清晰,是现代项目里值得优先考虑的方案。它天然不会有 flex 那种「被压缩」的问题。

1.5 方案三:Float 浮动布局(最古老,需理解 BFC)

Float 是「上古」方案,但它能帮我们真正吃透 BFC,所以值得讲。核心思路:左右两列分别 float: left / float: right,中间列触发 BFC 来避开浮动。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>三列布局 - Float</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }

    .layout { height: 100vh; }

    .sidebar {
      width: 200px;
      height: 100vh;
    }
    .left  { float: left;  background: #f0c0c0; }
    .right { float: right; background: #c0c0f0; }

    /* 中间列:触发 BFC,避免被浮动元素覆盖 */
    .content {
      overflow: hidden;   /* 关键:开启新的 BFC */
      background: #fff;
    }
  </style>
</head>
<body>
  <!-- 注意:float 方案必须让「浮动元素」写在前面 -->
  <div class="layout">
    <aside class="sidebar left">左侧导航</aside>
    <aside class="sidebar right">右侧广告</aside>
    <main class="content">中间主内容(自适应)</main>
  </div>
</body>
</html>

代码思路解析:

  1. 左列 float: left、右列 float: right,两者脱离文档流,分别贴在容器左右两侧;
  2. 中间列如果不做处理,会被浮动元素「盖住」,所以给它 overflow: hidden------这会让它开启一个新的 BFC,从而不再与浮动元素重叠,自动占满剩余空间。

两个必须记住的坑:

  • DOM 顺序变了:float 要求「浮动元素」必须写在前面,否则浮不上去。这跟「主内容优先渲染」的诉求是冲突的,所以 float 方案其实不利于语义化,这也是它逐渐被淘汰的原因之一;
  • overflow: hidden 的作用是触发 BFC,不是单纯「裁剪溢出」。很多同学以为它只是隐藏滚动条,其实它的真正作用是建立独立的排版上下文。

1.6 三种方案对比

方案 代码量 语义化 是否需处理「压缩」问题 兼容性 适用场景
Flex 好,可保持 main 居中 flex-shrink: 0 极好 最常用,推荐
Grid 最好 现代项目首选
Float 差,浮动弹需前置 overflow: hidden 触发 BFC 极好 理解 BFC / 老代码维护

小结:能写 Grid 优先 Grid,其次 Flex,Float 主要用于理解原理和阅读历史代码。


二、TypeScript 工具类型

2.1 为什么需要工具类型

在大型项目里,「类型」会被大量复用。假如我们有一个用户对象接口:

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

实际开发中,经常遇到这类需求:

  • 列表页只展示 idname,不想暴露全部字段;
  • 接口返回给前端时,要剔除 掉敏感字段(比如 email);
  • 更新用户信息时,所有字段都可选(传哪个改哪个);
  • 定义「状态码 → 错误提示」这种字典。

如果每次都手写一个新接口,字段一多就会又啰嗦又容易漏改。TypeScript 内置了一批工具类型(Utility Types),专门用来「基于已有类型,加工出新类型」。下面逐个讲清楚。

2.2 基础:keyof 与 typeof

理解工具类型前,先掌握两个基础操作符。

keyof:取出一个类型「所有键名」组成的联合类型。

typescript 复制代码
type UserKeys = keyof User;
// 等价于:type UserKeys = 'id' | 'name' | 'age' | 'email'

typeof(类型上下文里):取出一个「值」的类型。

typescript 复制代码
function fn() {
  return { x: 1, y: 2 };
}
type FnResult = ReturnType<typeof fn>; // { x: number; y: number }

注意:这里的 typeof 是「类型层面」的用法,和 JS 里的运行时 typeof 是两码事,别混淆。

2.3 Pick:挑选字段

Pick<T, K> 从类型 T挑选 出键集合 K 中的字段,组成新类型。

typescript 复制代码
// 从 User 中只挑出 id 和 name
type UserPreview = Pick<User, 'id' | 'name'>;

const u: UserPreview = {
  id: 1,
  name: 'zmt',
};
// 如果写成 { id: 1, name: 'zmt', age: 18 } 会报错:age 不存在于 UserPreview

底层实现(可略读,理解即可):

typescript 复制代码
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

[P in K] 是「映射类型」:遍历 K 里的每个键,用 T[P] 取出对应的值类型。K extends keyof T 约束 K 必须是 T 的键,防止写错。

2.4 Omit:剔除字段

Omit<T, K> 与 Pick 相反,从类型 T剔除 掉键集合 K,保留其余字段。

typescript 复制代码
// 从 User 中剔除 email,得到不含敏感字段的安全类型
type UserSafe = Omit<User, 'email'>;

const safeUser: UserSafe = {
  id: 1,
  name: 'zmt',
  age: 18,
};
// email 已被剔除,写 email 会报错

典型场景:接口返回给前端的 DTO(数据传输对象)通常要 Omit 掉密码、邮箱等敏感字段。

2.5 重点:Omit = Pick + Exclude

这是本章最核心、也最容易被问到的一条等价关系:

复制代码
Omit<T, K>  等价于  Pick<T, Exclude<keyof T, K>>

这条式子怎么理解?分三步拆解:

  1. keyof T :拿到 T 所有键的联合类型,即 'id' | 'name' | 'age' | 'email'
  2. Exclude<keyof T, K> :从这些键里删掉 要剔除的 K(比如 'email'),剩下要保留的键 'id' | 'name' | 'age'
  3. Pick<T, 剩下的键> :再用 Pick 把剩下的键从 T 里挑出来。

「剔除 = 先算出要保留的键,再挑选」------Omit 本质上是 Pick 的一个「组合拳」,它自己没有独立的新能力,只是把 Exclude 和 Pick 串了起来。TypeScript 内部正是这么实现的:

typescript 复制代码
// TS 官方 Omit 的等价实现
type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

用代码验证一下,结果完全一致:

typescript 复制代码
type UserKeys = keyof User;              // 'id' | 'name' | 'age' | 'email'
type KeepKeys = Exclude<UserKeys, 'email'>; // 'id' | 'name' | 'age'
type MyOmitUser = Pick<User, KeepKeys>;  // 等价于 Omit<User, 'email'>

再单独看 Exclude 的底层实现,加深理解:

typescript 复制代码
type Exclude<T, U> = T extends U ? never : T;

它利用「条件类型的分配律 」:当 T 是联合类型时,会逐个 判断 T 的每个成员是否属于 U,属于就变成 never(相当于丢弃),不属于就保留。比如 Exclude<'id'|'name'|'age'|'email', 'email'>,前三个都不等于 'email' 所以保留,'email' 自己等于 'email' 所以变成 never 被丢弃。

2.6 Partial:全字段可选

Partial<T> 把类型 T所有字段都变成可选

typescript 复制代码
type PartialUser = Partial<User>;

// 现在所有字段都可选,可以只传部分字段
const patchUser: PartialUser = {
  name: '张三',
  age: 18,
};

// 甚至可以传一个空对象,完全合法
const emptyObj: PartialUser = {};

典型场景: 更新接口(patch)经常只传「要修改的那几个字段」,用 Partial 表达最合适。

底层实现:

typescript 复制代码
type Partial<T> = {
  [P in keyof T]?: T[P];   // 注意多了一个 ?,表示可选
};

对比 Pick 的实现,差别只在 T[P] 后面多了一个 ?。理解这个「映射类型 + 可选修饰符」的组合,Pick/Omit/Partial 就全通了。

2.7 Record 与 ReturnType

Record<K, V>:构造一个「键类型为 K、值类型为 V」的对象类型,常用于字典 / 映射表。

typescript 复制代码
// 键是 string,值是 number
type Dict = Record<string, number>;
const obj: Dict = { a: 1, b: 2 };

// 经典场景:HTTP 状态码 → 错误提示
type ErrorMsgMap = Record<number, string>;
const errorMsgMap: ErrorMsgMap = {
  400: '请求参数错误',
  401: '未登录,请重新登录',
  403: '权限不足,拒绝访问',
  404: '资源找不到',
  500: '服务器内部错误,请稍后重试',
};

// 配合函数使用,取不到时给兜底值
function getErrMsg(code: number): string {
  return errorMsgMap[code] ?? '未知错误';
}

补充一个小知识:HTTP 状态码的分段含义------1XX 执行中、2XX 成功、3XX 重定向、4XX 客户端错误、5XX 服务端错误。上面 ErrorMsgMap 就是用 Record 把这套映射固化成了类型安全的字典。

ReturnType<T>:取出一个函数的「返回值类型」。

typescript 复制代码
function fn() {
  return { x: 1, y: 2 };
}
type FunReturn = ReturnType<typeof fn>; // { x: number; y: number }

它依赖 infer 推断,底层实现长这样(了解即可):

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

2.8 Exclude 与 Omit 的区别

很多同学分不清这两个,一句话点破:

  • Exclude 操作的是「联合类型」:从一串键(或字面量)里删除某些成员;
  • Omit 操作的是「对象 / 接口类型」:从一个对象的字段里删除某些键。

所以它们的「输入类型」不同、产出也不同:Exclude<'a'|'b'|'c', 'c'> 得到 'a'|'b'Omit<Obj, 'c'> 得到一个新的对象类型。而 Omit 内部正是「借用了 Exclude 删除键,再用 Pick 重建对象」,这就是 2.5 节那条等价关系的由来。


三、全文总结

这篇文章围绕两条主线展开:

  1. 三列布局 :核心是「左右固定、中间自适应」。实现它的底层原理是格式化上下文 ------<html> 根元素开启第一个 BFC,块级元素默认只能从上到下排列;要左右分列,就必须通过 display: flex(FFC)、display: grid(GFC)、float / overflow: hidden 等方式开启新的格式化上下文。三种方案里,Grid 最简洁、Flex 最常用、Float 最古老但最能帮助理解 BFC。

  2. TS 工具类型 :核心是「基于已有类型加工新类型」。Pick 挑字段、Omit 删字段、Partial 全转可选、Record 造字典、ReturnType 取返回值类型。其中最重要的等价关系是 Omit<T,K> = Pick<T, Exclude<keyof T, K>>------它说明 Omit 不是「原生能力」,而是 Exclude(删键)+ Pick(挑键)的组合拳,理解了它,整套工具类型就串起来了。


四、核心知识点复盘

知识点 一句话结论
文档流 块级元素从上到下、行内元素从左到右
BFC / FFC / GFC 独立的排版作用域,html 根元素开启第一个 BFC
开启新格式化上下文 flex / grid / float / position / overflow: hidden
三列布局 左右定宽 + 中间自适应(flex: 1 / 1fr / overflow: hidden
flex-shrink: 0 防止 flex 子项被压缩、固定宽度失效
keyof T 取出 T 所有键组成的联合类型
Pick<T, K> 挑选 K 里的字段,组成新类型
Omit<T, K> 剔除 K 里的字段
Exclude<T, U> 从联合类型 T 中删除属于 U 的成员
等价关系 Omit<T,K> = Pick<T, Exclude<keyof T, K>>
Partial<T> 所有字段转可选,用于 patch 更新
Record<K, V> 构造键 K 值 V 的字典类型
ReturnType<typeof fn> 取函数返回值类型

五、常见问题 / 避坑指南

Q1:三列布局里中间列没撑满 / 左右列被压缩了怎么办?

A:最常见原因是 flex 方案里漏写了 flex-shrink: 0 。flex 子项默认 flex-shrink: 1(允许收缩),当空间不足时左右两列会被挤窄。给固定宽度的 .sidebar 加上 flex-shrink: 0 即可。Grid 方案则天然没有这个问题。

Q2:overflow: hidden 在三列布局里到底起了什么作用?

A:它不是「隐藏滚动条」那么简单,而是触发 BFC 。float 布局中,浮动元素会脱离文档流、盖住后面的普通元素;给中间列加 overflow: hidden 会让它开启新的 BFC,从而不再与浮动元素重叠,自动占满剩余宽度。这是理解 float 布局的关键。

Q3:为什么 float 方案里 <main> 要写在两个 aside 后面?

A:float 要求「浮动元素必须出现在文档流中更靠前的位置」才能正常浮起来。这跟「主内容优先渲染」的诉求相冲突,也是 float 被 flex / grid 取代的重要原因之一。

Q4:keyoftypeof 老分不清怎么办?

A:一句话区分------keyof 取「键」,typeof 取「类型」keyof User 得到键名联合类型;typeof 变量(在类型位置)得到该变量的类型。ReturnType<typeof fn> 就是「先拿到 fn 的类型,再取它的返回值类型」。

Q5:ExcludeOmit 到底什么时候用哪个?

A:操作对象是「联合类型」用 Exclude(删几个字面量成员);操作对象是「对象 / 接口」用 Omit(删几个字段)。记不住也没关系,记住 Omit = Pick + Exclude 这条关系,一切都能倒推出来。

Q6:Partial 之后字段都可选,会不会丢失类型安全?

A:不会「丢失」,反而是「放宽」。Partial<User> 要求你写出来的每个字段(如果有)类型仍然正确,只是允许你不写 某些字段。更新接口(patch 语义)用它是安全的,读取/展示场景则应该用完整的 UserPick/Omit 出来的类型,别滥用。

Q7:这些工具类型需要自己写吗?

A:不需要。它们都是 TypeScript 内置的,直接使用即可。但理解它们的底层实现 (尤其是映射类型 [P in keyof T] 和条件类型 T extends U ? X : Y)能让你举一反三,以后遇到 RequiredReadonlyRecord 等同类工具类型时,一眼就能看懂本质。

相关推荐
名字还没想好☜1 小时前
Next.js Route Handler 做 SSE 服务端推送:实时进度条、自动重连与什么时候别用 WebSocket
开发语言·javascript·websocket·react·sse·next.js
IMPYLH1 小时前
HTML 的 <html> 元素
前端·javascript·html
IT_陈寒1 小时前
SpringBoot自动配置的坑,把我整不会了
前端·人工智能·后端
赵大仁1 小时前
Human-in-the-loop:前端确认流与后端幂等
前端·后端·ai·agent·人机协作
恋猫de小郭1 小时前
超好用 R8 Configuration Analyzer, 优化你的 App 大小和内存
android·前端·flutter
赖龙10 小时前
pnpm vs npm
前端·npm·node.js
To_OC11 小时前
LC 239 滑动窗口最大值:从暴力超时到单调队列一遍过
javascript·算法·leetcode
学如逆水,不进则退11 小时前
在线免费音频剪辑:NBTools 可视化波形裁剪 MP3/WAV,本地处理免上传
前端·音视频
Mh12 小时前
虚拟滚动真的比普通滚动性能更好吗?
前端·javascript·性能优化