2 道大厂面试题:TS 工具类型我懂了,CSS 3 列布局把我问住了
一道 TS,一道 CSS。
TS 那道我顺手写完了。CSS 那道我卡在 BFC------笔记里只记了个名词,真让我写代码我下不去手。这篇文章把两道题都摆出来,先讲我懂的那道,再讲我怎么把 CSS 这道啃下来的。
题一:Omit<T,K> 等价于 Pick<T,Exclude<keyof T,K>> 怎么理解?
原题就一句话:
Omit<T,K>等价于Pick<T,Exclude<keyof T,K>>,怎么理解?
这道题不是让你背 Omit 的源码,是考你能不能把一个工具类型拆开,说清楚每一层在做什么。我的思路是:先把所有用到的工具类型都过一遍,再回到这个等价式。
先把工具类型认全
给一个 User 接口,然后用各种工具类型从它身上"切片":
ts
// 详细的完整的信息
interface User{
id: number;
name: string;
age: number;
email: string;
}
工具类型 :TS 自带的一类泛型类型,输入一个类型、返回一个新类型。本质是"类型的函数"。下面这些是面试常考的七个:
Pick / Omit / Partial / Exclude / Record / ReturnType / keyof。
Pick ------ 从一个类型里挑部分属性,组成新类型。大型项目里前端不同组件需要不同子集,比如列表页只要 id 和 name,详情页才要全部字段,这就是 Pick 的用武之地。
ts
// 子类型
// 可被看到的信息
type UserPreview = Pick<User, 'id' | 'name'>
const userPreview: UserPreview = {
id: 1,
name: '张三',
}
联合类型 :用
|连接的类型,表示"只能是这几个里的一个"。'id' | 'name'表示这个值要么是'id'字符串,要么是'name'字符串。Pick 的第二个参数吃的就是联合类型。
Omit ------ Pick 的反面,从一个类型里排除部分属性。
ts
// Omit 类型,从一个类型中排除部分属性,组成一个新的类型
type UserSafe = Omit<User, 'email'>
const userSafe: UserSafe = {
id: 2,
name: '李四',
age: 25,
}
UserSafe 把 email 排掉了------后端返回给前端的时候,敏感字段别带出去。
Partial ------ 把一个类型的全部字段变成可选。它的典型场景是 Patch 修改:原对象属性很多,你不知道用户这次要改哪几个字段,干脆全设成可选。
ts
// Partial 类型,将 user 的全部字段变成可选的
type PartialUser = Partial<User>
// Patch 修改,原对象属性很多,无法具体的指定需要修改的属性,此时使用 Partial 类型
const patchUser: PartialUser = {
name: '王五',
age: 28,
}
const emptyObj: PartialUser = {};
连一个空对象 {} 都能赋值给 PartialUser------因为所有字段都可选了。
Record ------ 哈希表类型,Record<键类型, 值类型>。我笔记里写了两个用例,第二个用 HTTP 状态码做 key,比较有意思:
ts
// json key:value Record<键类型,值类型> 哈希表类型
type DictRecord = Record<string, number>;
const obj: DictRecord = {
a: 1,
b: 2,
}
type ErrorMsgMap = Record<number, string>;
const errorMessage: ErrorMsgMap = {
// 错误码()
// http status code 状态码
// 1xx 执行中
// 2xx 成功
// 3xx 重定向,要跳转
// 4xx 客户端错误
// 5xx 服务器错误
400: '请求参数错误', // 登录
401: '无授权,未登录',
403: '没有权限,权限不足',
404: '资源找不到',
500: '服务器内部错误',
}
配套还有个取错误信息的小函数:
ts
function getErrorMsg(code: number): string {
return errorMessage[code] ?? '未知错误'; // ?? 空值合并运算符
}
空值合并运算符
??:左边的值为null或undefined时才取右边的。和||的区别是------||会把0、''、false这些 falsy 值也跳过,而??只跳null/undefined。这里errorMessage[code]可能是undefined(状态码不在表里),用??兜底成"未知错误"。
ReturnType ------ 拿函数返回值的类型。先定义个函数,再 typeof 取它的类型,再 ReturnType 取返回值类型:
ts
function fn(){
return {x:1,y:2}
}
// 获取函数的返回值类型
type fnReturn = ReturnType<typeof fn>
fnReturn 推出来就是 { x: number; y: number }。typeof 在类型上下文里不是 JS 那个判断类型的运行时运算符,而是"取这个值的类型"。
Exclude + keyof ------ 这两个才是题一真正的核心。
ts
// 联合类型,all 只能是 id, name, age, email 中的一个
type All = "id" | "name" | "age" | "email"
// Exclude 处理联合类型(字符串)
// Omit 处理的是类型,或者说对象,对象的接口
type AfterExecute = Exclude<All, "email">;
keyof T :取一个类型所有键名组成的联合类型。对
User来说,keyof User就是"id" | "name" | "age" | "email"。Exclude<U, E> :从联合类型
U里剔除E。注意它处理的是联合类型(一串字符串字面量),不是对象类型。
笔记里那行注释我得记下来,它点破了面试官最容易混淆的地方:Exclude 处理联合类型,Omit 处理对象/接口。
回到等价式
工具类型都认识了,再看 Omit<T,K> = Pick<T, Exclude<keyof T, K>> 就清晰了:
keyof T------ 把 T 的所有键名联合起来,比如User→"id" | "name" | "age" | "email"Exclude<keyof T, K>------ 把要剔除的 K 从联合类型里删掉,剩下要保留的键Pick<T, 剩下的键>------ 用 Pick 把剩下的键从 T 里挑出来,组成新类型
三步走完,就是 Omit 的效果。
手写一遍就懂了
手写一遍。2.ts 这么写:
ts
interface User{
id: number;
name: string;
age: number;
email: string;
}
type UserKeys = keyof User;
type KeepKeys = Exclude<UserKeys, "email">;
type MyOmitUser = Pick<User, KeepKeys>;
三行拆开:
UserKeys把所有键名联合起来KeepKeys用 Exclude 删掉"email",剩下"id" | "name" | "age"MyOmitUser用 Pick 把剩下的键从User里挑出来
跑一下,MyOmitUser 的结构和 Omit<User, "email"> 一模一样。
这道题我懂了,写起来顺手。下一道就没这么轻松了。
题二:3 列布局,我卡在 BFC
题目本身一句话:实现 3 列布局。
PC 端常见的布局方案,左右两列指定宽度,中间的是响应式自适应宽度(弹性)。听起来不难,但藏了个隐藏考点。
题目真正在问什么
笔记里这一行才是题眼:
最好让 main(center)最先加载,加强 SEO,最快看到最有效的内容 左右两侧往往是广告、导航,可以晚一点
所以这道题的真正考点不是"3 列能不能写出来",而是:HTML 结构上 <main> 必须排第一个,但视觉上要排在中间。
为什么这是考点?因为最直觉的 flex 写法是这样的:
html
<div class="layout">
<aside class="left">左</aside>
<main class="center">中间</main>
<aside class="right">右</aside>
</div>
css
.layout { display: flex; }
.left, .right { width: 200px; }
.center { flex: 1; }
视觉上没问题。但 HTML 顺序是 left → center → right,<main> 排在第二位------SEO 考点直接挂掉。爬虫读 DOM 顺序读,最重要的内容应该最先出现。
所以真题目是:HTML 让 <main> 在最前,视觉让 <main> 在中间。这一步想不通,后面所有方案都看不懂。
BFC 是什么,我之前只记了个名词
要解这道题,绕不开 BFC。
BFC(Block Formatting Context,块级格式化上下文):一个独立的渲染区域,内部的元素怎么排布,不会影响外面;外面的元素,也不会影响里面。最值钱的特性就是这两个字------隔离。
课上老师的讲解从根上开始:
html 开始,根,开启了第一个格式化上下文 BFC 块级元素从上到下,行内元素从左到右排列,形成最基本的文档流
也就是说,html 根元素开了第一个 BFC,决定了块级元素从上到下排、行内从左到右排------这是文档流的基本规则。
但"在一个块中加多列?"------inline 不适合做盒子。要开启新的、局部的格式化上下文,让这一块按自己的规则排布,不被外面影响。
开启新 BFC 的方式笔记里列了:
block 元素 + display 属性 :flex、grid、table...
float:left、position、overflow:hidden都可以开启一个新的 BFC
新 BFC 的关键特性是「外层 BFC 不会影响内部的新的 BFC」------这就是它能做复杂局部布局的根本。
GFC(Grid Formatting Context,网格格式化上下文) :
display: grid开启的格式化上下文,可以直接定义二维布局。我 2.html 的注释里就写了一行------"GFC 直接可以定义二维布局的方案",当时没展开,下面 grid 方案就是它。
三种实现方案
方案一:圣杯布局(BFC + float + 负 margin)
最经典,也是面试官最想听到的。HTML 让 <main> 排第一:
html
<div class="layout">
<main class="center">中间内容</main>
<aside class="left">左</aside>
<aside class="right">右</aside>
</div>
CSS 思路:
css
.layout { overflow: hidden; /* 父容器开启 BFC,包裹浮动 */ }
.center {
width: 100%; /* 先占满一行 */
margin-left: 200px; /* 左侧给 left 留位置 */
margin-right: 200px; /* 右侧给 right 留位置 */
box-sizing: border-box;
}
.left, .right {
width: 200px;
float: left; /* 浮动起来,脱离文档流 */
}
.left { margin-left: -100%; /* 负 margin 把 left 拉回最左 */ }
.right { margin-left: -200px; /* 负 margin 把 right 拉到最右 */ }
负 margin 的作用 :
center占了 100% 宽度,left浮动后本该在center下方右侧。margin-left: -100%把left往左拽一整个父容器宽度,正好回到最左边;right用-200px(自己宽度)同理拽到最右。
这就是圣杯/双飞翼家族的原理。
方案二:Flex + order(最直观)
HTML 还是 <main> 在第一个,用 order 调视觉顺序:
css
.layout { display: flex; } /* 开启 FFC,flex 格式化上下文 */
.left { width: 200px; order: 1; }
.center { flex: 1; order: 2; }
.right { width: 200px; order: 3; }
order :flex/grid 项的视觉顺序,默认按 HTML 顺序 0、1、2...。改
order只改视觉,不改 DOM 顺序------所以<main>在 HTML 里还是第一个,爬虫读到的还是 main 在前,SEO 不受影响。
这个方案最简单,也是 Vibe Coding 时代 AI 最可能给你的写法。
方案三:Grid(最简洁)
css
.layout {
display: grid; /* 开启 GFC */
grid-template-columns: 200px 1fr 200px; /* 三列:左定宽 中弹性 右定宽 */
grid-template-areas: "left center right"; /* 命名区域 */
}
.left { grid-area: left; }
.center { grid-area: center; }
.right { grid-area: right; }
DOM 顺序 main 在前都没事------grid-area 直接把元素钉到指定位置,跟 HTML 顺序无关。GFC 就是这么横。
两道题摆一起,我看到了什么
两道题放一起看,有意思的地方就出来了。
TS 那道,工具类型是已经封装好的抽象 。Omit 就是一个"类型的函数",输入旧类型输出新类型,源码本身就是 Pick + Exclude + keyof 的组合。面试官考你的是能不能拆开抽象、看清每层在做什么------这是读源码的能力。
CSS 那道正好相反。BFC 是个已经存在的底层机制 ,面试官考你的是能不能基于机制自己组装 出方案。圣杯布局的负 margin、flex 的 order、grid 的 grid-area------同一个目标,三种抽象层级。这是写实现的能力。
Vibe Coding 时代这两道题给我提了个醒:让 AI 写个 3 列布局,它大概率直接给你 flex + flex: 1,HTML 顺序就是 left → center → right,SEO 考点悄无声息地挂掉。AI 给的是"看起来对的",不是"理解了考点的"。你得能看出它哪里偷懒了,否则面试场上你写出来的东西和 AI 没区别,那就只剩被刷。
TS 这边 AI 倒是好帮手------Pick<T, Exclude<keyof T, K>> 这种等价式,让它解释每一层在做什么,比死记强。但前提是你自己得能拆一遍,否则 AI 讲完你也只是"听过",不是"会写"。
读懂别人封装好的抽象,自己又能从底层机制组装出方案------这两条线,是这个时代程序员不会被 AI 替代的部分。
术语速查
- 工具类型:TS 自带的"类型的函数",输入一个类型、返回一个新类型。常见七个:Pick / Omit / Partial / Exclude / Record / ReturnType / keyof。
- 联合类型 :
A | B | C,表示值只能是这几个里的一个。keyof 的返回值、Exclude 的输入都是联合类型。 - keyof T:取 T 的所有键名组成的联合类型。
- Exclude<U, E>:从联合类型 U 中剔除 E。处理联合类型,不处理对象。
- Pick<T, K>:从对象类型 T 中挑出 K 这些键,组成新类型。
- Omit<T, K> :从对象类型 T 中排除 K 这些键。等价于
Pick<T, Exclude<keyof T, K>>。 - Partial<T>:把 T 所有字段变成可选。
- Record<K, V>:键类型 K、值类型 V 的哈希表类型。
- ReturnType<F>:取函数 F 的返回值类型。
- 空值合并运算符
??:左边为null/undefined才取右边,比||更精确(不会误伤0、''、false)。 - BFC(Block Formatting Context) :块级格式化上下文,独立的渲染区域,内外互相不影响。
display:flex/grid、float、position:absolute/fixed、overflow:hidden等都能开启新 BFC。 - GFC(Grid Formatting Context) :
display: grid开启的网格格式化上下文,支持二维布局。 - order:flex/grid 项的视觉顺序属性,改 order 只改视觉、不改 DOM 顺序,因此不影响 SEO。
- 圣杯/双飞翼布局:经典 3 列布局方案家族,都用 float + 负 margin 实现 main 先加载、视觉居中。两者区别在留位方式------圣杯用父容器 padding + relative 偏移,双飞翼用 main 外包一层 + main 自身 margin。