2 道大厂面试题:TS 工具类型我懂了,CSS 3 列布局把我问住了

0 阅读11分钟

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 接口,然后用各种工具类型从它身上"切片":

// 详细的完整的信息
interface User{
    id: number;
    name: string;
    age: number;
    email: string;
}

工具类型:TS 自带的一类泛型类型,输入一个类型、返回一个新类型。本质是"类型的函数"。下面这些是面试常考的七个:Pick / Omit / Partial / Exclude / Record / ReturnType / keyof

Pick —— 从一个类型里挑部分属性,组成新类型。大型项目里前端不同组件需要不同子集,比如列表页只要 idname,详情页才要全部字段,这就是 Pick 的用武之地。

// 子类型
// 可被看到的信息
type UserPreview = Pick<User, 'id' | 'name'>
const userPreview: UserPreview = {
    id: 1,
    name: '张三',
}

联合类型:用 | 连接的类型,表示"只能是这几个里的一个"。'id' | 'name' 表示这个值要么是 'id' 字符串,要么是 'name' 字符串。Pick 的第二个参数吃的就是联合类型。

Omit —— Pick 的反面,从一个类型里排除部分属性。

// Omit 类型,从一个类型中排除部分属性,组成一个新的类型
type UserSafe = Omit<User, 'email'>
const userSafe: UserSafe = {
    id: 2,
    name: '李四',
    age: 25,
}

UserSafeemail 排掉了——后端返回给前端的时候,敏感字段别带出去。

Partial —— 把一个类型的全部字段变成可选。它的典型场景是 Patch 修改:原对象属性很多,你不知道用户这次要改哪几个字段,干脆全设成可选。

// Partial 类型,将 user 的全部字段变成可选的
type PartialUser = Partial<User>
// Patch 修改,原对象属性很多,无法具体的指定需要修改的属性,此时使用 Partial 类型
const patchUser: PartialUser = {
    name: '王五',
    age: 28,
}

const emptyObj: PartialUser = {};

连一个空对象 {} 都能赋值给 PartialUser——因为所有字段都可选了。

Record —— 哈希表类型,Record<键类型, 值类型>。我笔记里写了两个用例,第二个用 HTTP 状态码做 key,比较有意思:

// 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: '服务器内部错误',
}

配套还有个取错误信息的小函数:

function getErrorMsg(code: number): string {
    return errorMessage[code] ?? '未知错误'; // ?? 空值合并运算符
}

空值合并运算符 ??:左边的值为 nullundefined 时才取右边的。和 || 的区别是——|| 会把 0''false 这些 falsy 值也跳过,而 ?? 只跳 null/undefined。这里 errorMessage[code] 可能是 undefined(状态码不在表里),用 ?? 兜底成"未知错误"。

ReturnType —— 拿函数返回值的类型。先定义个函数,再 typeof 取它的类型,再 ReturnType 取返回值类型:

function fn(){
    return {x:1,y:2}
}

// 获取函数的返回值类型
type fnReturn = ReturnType<typeof fn>

fnReturn 推出来就是 { x: number; y: number }typeof 在类型上下文里不是 JS 那个判断类型的运行时运算符,而是"取这个值的类型"。

Exclude + keyof —— 这两个才是题一真正的核心。

// 联合类型,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>> 就清晰了:

  1. keyof T —— 把 T 的所有键名联合起来,比如 User"id" | "name" | "age" | "email"
  2. Exclude<keyof T, K> —— 把要剔除的 K 从联合类型里删掉,剩下要保留的键
  3. Pick<T, 剩下的键> —— 用 Pick 把剩下的键从 T 里挑出来,组成新类型

三步走完,就是 Omit 的效果。

手写一遍就懂了

手写一遍。2.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 写法是这样的:

<div class="layout">
  <aside class="left"></aside>
  <main class="center">中间</main>
  <aside class="right"></aside>
</div>
.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:leftpositionoverflow:hidden 都可以开启一个新的 BFC

新 BFC 的关键特性是「外层 BFC 不会影响内部的新的 BFC」——这就是它能做复杂局部布局的根本。

GFC(Grid Formatting Context,网格格式化上下文)display: grid 开启的格式化上下文,可以直接定义二维布局。我 2.html 的注释里就写了一行——"GFC 直接可以定义二维布局的方案",当时没展开,下面 grid 方案就是它。

三种实现方案

方案一:圣杯布局(BFC + float + 负 margin)

最经典,也是面试官最想听到的。HTML 让 <main> 排第一:

<div class="layout">
    <main class="center">中间内容</main>
    <aside class="left"></aside>
    <aside class="right"></aside>
</div>

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 调视觉顺序:

.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(最简洁)
.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/gridfloatposition:absolute/fixedoverflow:hidden 等都能开启新 BFC。
  • GFC(Grid Formatting Context)display: grid 开启的网格格式化上下文,支持二维布局。
  • order:flex/grid 项的视觉顺序属性,改 order 只改视觉、不改 DOM 顺序,因此不影响 SEO。
  • 圣杯/双飞翼布局:经典 3 列布局方案家族,都用 float + 负 margin 实现 main 先加载、视觉居中。两者区别在留位方式——圣杯用父容器 padding + relative 偏移,双飞翼用 main 外包一层 + main 自身 margin。