TS 进阶类型的实用用法:联合、交叉、可选、「Partial、Pick」 等

0 阅读7分钟

同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~

(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)

你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?

你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?

就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。

一天只有24小时,时间永远不够用,常常感到力不从心。

技术行业,本就是逆水行舟,不进则退。

如果你也有同样的困扰,别慌。

从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲

这一次,我们一起慢慢来,扎扎实实变强。

不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,

咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。

一、开篇:为什么需要进阶类型

TypeScript 进阶类型不是炫技,而是为了:

  1. 请求参数:接口字段多、可选字段多,需要清晰约束
  2. 组件 Props:组件变多,属性要可读、可复用、易维护
  3. 表单模型:数据和校验规则耦合,类型要统一、少写重复

后面会围绕这三种场景,讲联合、交叉、可选、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();  // 提交时类型正确

九、踩坑总结

场景问题建议
联合类型未做类型收窄直接访问属性ifswitch 或类型守卫区分
交叉类型同名属性类型冲突避免冲突,或改用继承/联合
Partial更新时漏掉必填校验提交前做运行时校验
Pick/Omit源类型改动后忘记同步尽量基于同一基础类型派生
可选属性?.! 混用导致误判先理解“可选”语义,再决定是否断言

十、小结

  • 联合类型:多选一,配合可区分字段做类型收窄
  • 交叉类型:多类型合并,注意同名属性
  • 可选属性:用 ? 表示“可以不传”
  • Partial:全变可选,常用于更新参数
  • Pick:保留部分字段,常用于列表项、安全字段
  • Omit:排除部分字段,常用于创建 DTO

请求参数、组件 Props、表单模型,都可以在这些类型上组合、复用,减少重复定义和运行时错误。


学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。

后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。

关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。

如果你觉得这篇内容对你有帮助,不妨点赞收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。

我是 Eugene,你的电子学友,我们下一篇干货见~