同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有24小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,
咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。
一、开篇:为什么需要进阶类型
TypeScript 进阶类型不是炫技,而是为了:
- 请求参数:接口字段多、可选字段多,需要清晰约束
- 组件 Props:组件变多,属性要可读、可复用、易维护
- 表单模型:数据和校验规则耦合,类型要统一、少写重复
后面会围绕这三种场景,讲联合、交叉、可选、Partial、Pick 等的实际用法。
二、联合类型 Union:二选一或多选一
2.1 是什么
联合类型表示“取其一”,用 | 连接多个类型。
// 状态只能是这几种之一
type Status = 'pending' | 'success' | 'error';
// 按钮类型
type ButtonType = 'primary' | 'default' | 'dashed';
// 混合联合
type ID = string | number;
2.2 常见用法:区分不同 payload
接口返回不同结构时,用联合类型区分:
// 用户列表接口:成功和失败结构不同
type UserListResponse =
| { code: 0; data: { list: User[]; total: number } }
| { code: 1; message: string };
function handleResponse(res: UserListResponse) {
if (res.code === 0) {
// 这里 TypeScript 知道一定有 data
console.log(res.data.list);
} else {
// 这里知道一定有 message
console.log(res.message);
}
}
关键点:通过 code 这种“可区分字段”做类型收窄,TS 能推断出各自分支里的类型。
2.3 坑点:字面量 vs 普通类型
// 正确:明确是字面量
const status: 'pending' | 'success' = 'pending';
// 危险:类型是 string,不是字面量
let status2 = 'pending'; // 类型是 string
status2 = 'any string'; // 可以赋值任意字符串
// 更安全的写法
const status3 = 'pending' as const; // 类型为 'pending'
实际开发中,尽量用 as const 或显式字面量类型,避免误用。
三、交叉类型 Intersection:类型叠加
3.1 是什么
交叉类型用 & 合并多个类型,得到“同时满足所有类型”的新类型。
type A = { name: string };
type B = { age: number };
type C = A & B; // { name: string; age: number }
3.2 常见用法:组合基础类型
// 基础配置
type BaseConfig = { baseUrl: string; timeout: number };
// 扩展配置
type AuthConfig = { token?: string };
// 合并成完整配置
type ApiConfig = BaseConfig & AuthConfig;
const config: ApiConfig = {
baseUrl: '/api',
timeout: 5000,
token: 'xxx', // 可选
};
适合做“公共基础 + 具体扩展”的模式。
3.3 坑点:同名属性冲突
type A = { id: number };
type B = { id: string };
type C = A & B; // id 变成 never(number & string 不可能同时成立)
// 实际中要避免这种冲突,或改用联合/继承
交叉类型合并时,同名属性的类型会做“交叉”,矛盾类型会变成 never,需要注意。
四、可选属性:什么时候用 ?
4.1 基础
interface User {
id: number;
name: string;
avatar?: string; // 可选
}
4.2 请求参数中的典型用法
// 列表查询:分页、排序、筛选都可选
interface ListQuery {
page?: number;
pageSize?: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
keyword?: string;
}
// 创建用户:部分必填、部分可选
interface CreateUserDto {
username: string; // 必填
password: string; // 必填
nickname?: string; // 可选
avatar?: string;
}
4.3 和 undefined 的区别
interface A {
name?: string; // 可以不传,也可以是 undefined
}
interface B {
name: string | undefined; // 必须显式传(哪怕是 undefined)
}
const a: A = {}; // OK
const b: B = { name: undefined }; // 必须写 name
const b2: B = {}; // 错误:缺少 name
在 API 参数里,通常用 ? 表示“可以不传”。
五、Partial / Required / Pick / Omit 等工具类型
5.1 Partial:全部变成可选
interface User {
id: number;
name: string;
email: string;
}
// 更新用户时,只传需要改的字段
type UpdateUserDto = Partial<User>;
// 等价于 { id?: number; name?: string; email?: string; }
function updateUser(id: number, data: UpdateUserDto) {
// data 可以是 {}、{ name: '新名字' } 等
}
常用场景:增删改查里的“更新接口参数”。
5.2 Required:全部变成必填
interface Config {
baseUrl?: string;
timeout?: number;
}
// 内部使用时要求一定存在
type RequiredConfig = Required<Config>;
// { baseUrl: string; timeout: number; }
5.3 Pick:只保留部分属性
interface User {
id: number;
name: string;
password: string;
email: string;
}
// 列表项只需要部分字段
type UserListItem = Pick<User, 'id' | 'name' | 'email'>;
const listItem: UserListItem = {
id: 1,
name: '张三',
email: 'zhangsan@example.com',
// 不需要 password
};
用于“大模型取子集”,避免暴露敏感字段。
5.4 Omit:排除部分属性
// 创建用户时不需要 id(后端生成)
type CreateUserDto = Omit<User, 'id'>;
// 排除多个
type UserWithoutSensitive = Omit<User, 'password' | 'id'>;
和 Pick 相反:不关心要什么,只关心不要什么时用 Omit。
5.5 组合使用
// 创建:不要 id,password 可选
type CreateUserDto = Partial<Pick<User, 'password'>> & Omit<User, 'id'>;
// 或者:创建时 password 必填,其他可选
type CreateUserDto2 = Pick<User, 'password'> & Partial<Omit<User, 'id' | 'password'>>;
根据业务“必填/可选/排除”需求自由组合。
六、实战一:请求参数类型
6.1 列表查询
// 通用分页参数
interface PaginationParams {
page?: number;
pageSize?: number;
}
// 用户列表筛选
interface UserFilter {
keyword?: string;
status?: 'active' | 'inactive';
role?: string;
}
// 组合
type UserListParams = PaginationParams & UserFilter;
async function fetchUserList(params: UserListParams) {
const { page = 1, pageSize = 10, ...rest } = params;
return request.get('/users', { params: { page, pageSize, ...rest } });
}
6.2 增删改查参数分层
interface User {
id: number;
username: string;
password: string;
nickname: string;
}
// 查询:ID 必填
type GetUserParams = Pick<User, 'id'>;
// 创建:不要 id,password 必填
type CreateUserDto = Omit<User, 'id'>;
// 更新:全部可选
type UpdateUserDto = Partial<Omit<User, 'id'>> & Pick<User, 'id'>;
这样每个接口的参数类型都清晰、可复用。
七、实战二:组件 Props 类型
7.1 基础 Props
interface ButtonProps {
type?: 'primary' | 'default' | 'dashed';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
children: React.ReactNode;
onClick?: () => void;
}
const Button: React.FC<ButtonProps> = ({ type = 'default', children, onClick }) => {
return <button onClick={onClick}>{children}</button>;
};
7.2 扩展 HTML 原生属性
// 在原生 button 基础上扩展
type NativeButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
type ButtonProps = NativeButtonProps & {
variant?: 'primary' | 'secondary';
loading?: boolean;
};
// 使用时可以传 className、onClick、disabled 等
7.3 条件 Props
// 有 href 是链接样式,没有是按钮
type LinkOrButtonProps =
| ({ as: 'link'; href: string } & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>)
| ({ as?: 'button' } & React.ButtonHTMLAttributes<HTMLButtonElement>);
八、实战三:表单模型类型
8.1 表单数据结构
interface LoginForm {
username: string;
password: string;
remember?: boolean;
}
// 初始值
const initialValues: LoginForm = {
username: '',
password: '',
remember: false,
};
8.2 与校验规则结合
// 用 Partial 表示“可能还没填完”的表单
type FormErrors = Partial<Record<keyof LoginForm, string>>;
function validateForm(values: Partial<LoginForm>): FormErrors {
const errors: FormErrors = {};
if (!values.username) errors.username = '请输入用户名';
if (!values.password) errors.password = '请输入密码';
return errors;
}
8.3 提交时的类型
// 提交时必填字段要齐全
function handleSubmit(values: LoginForm) {
// values 此时保证 username、password 存在
api.login(values);
}
// 如果用了 Ant Design Form
const [form] = Form.useForm<LoginForm>();
form.submit(); // 提交时类型正确
九、踩坑总结
| 场景 | 问题 | 建议 |
|---|---|---|
| 联合类型 | 未做类型收窄直接访问属性 | 用 if、switch 或类型守卫区分 |
| 交叉类型 | 同名属性类型冲突 | 避免冲突,或改用继承/联合 |
| Partial | 更新时漏掉必填校验 | 提交前做运行时校验 |
| Pick/Omit | 源类型改动后忘记同步 | 尽量基于同一基础类型派生 |
| 可选属性 | ?. 和 ! 混用导致误判 | 先理解“可选”语义,再决定是否断言 |
十、小结
- 联合类型:多选一,配合可区分字段做类型收窄
- 交叉类型:多类型合并,注意同名属性
- 可选属性:用
?表示“可以不传” - Partial:全变可选,常用于更新参数
- Pick:保留部分字段,常用于列表项、安全字段
- Omit:排除部分字段,常用于创建 DTO
请求参数、组件 Props、表单模型,都可以在这些类型上组合、复用,减少重复定义和运行时错误。
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~