最近在把负责的项目从js转成ts,一些经验记录,因为这个项目是新搭的,只有七八个路由页,总共十来个组件,改起来难度比较小(当然主要是我第一次自己改,自己搞就容易一切从简,只能说尽力做到除了接口泛型不用any)
工厂函数
工厂函数(Factory Function) 是一种「不依赖 new 关键字,通过普通函数封装对象 / 数据 / 实例的创建逻辑,最终返回目标结果」的设计模式。
在项目中我遇到最多的工厂模式的场景就是组件封装的时候使用的defineProps
interface PropsAction{
cc:string,
dd:number
}
const props = withDefaults(defineProps<{
aaa:string,
bbb:PropsAction
}>(),{
aaa:'',
bbb:()=>({
cc:''
dd:0
})
})
工厂模式整体上就是通过函数封装一个使用对象,来自豆包的例子
const createReactiveList = <T = DataType>(
defaultData: T[] = [] // 可选默认值,支持泛型(灵活适配不同类型)
): Ref<T[]> => {
// 内部封装:初始化 + 类型约束
const list = ref<T[]>(defaultData);
// 可额外封装操作逻辑(如添加、清空,按需扩展)
const addItem = (item: T) => {
list.value.push(item);
};
const clearList = () => {
list.value = [];
};
// 返回响应式数据 + 操作方法(或仅返回数据)
return {
list,
addItem,
clearList,
};
};
unknown和any的区别
unknown是ts的推荐+强安全性类型,any是不推荐的+无校验类型
unknown自带类型收缩,在真实的使用场景需要做类型校验才能够使用
const queryValue: unknown = route.query.aaa;
queryValue.toUpperCase();//会报错,无法使用,提示必须做类型检验
queryValue = queryValue + 1;//会报错,无法使用,提示必须做类型检验
// 🌟 类型收窄后,TS 允许使用(和你之前的查询参数处理逻辑一致)
if (typeof queryValue === 'string') {
queryValue.toUpperCase(); // ✅ 确认是 string 后,可安全调用方法
}
if (typeof queryValue === 'number') {
queryValue + 1; // ✅ 确认是 number 后,可安全运算
}
const queryValue2: any = route.query.aaa;
queryValue2.toUpperCase();//会正常通过不会报错提示
queryValue2 = queryValue2 + 1;//会正常通过不会报错提示
any一般应用的场景都在:
- 老项目迁移过渡用,方便快速转成ts,再支持后续迭代
- 第三方项目,不太清楚实际的内部结构,比如这次我的项目接的finebi平台,不知道一部分帆软的实际接口内部的类型定义,只能靠猜测一部分,一部分<T=any>去处理类型
类型守卫
本质上是在多种类型type的情况下,对于部分类型属性不兼容,导致需要收缩判断,去执行部分逻辑。
说人话就是:已知A数据一定是AAA|BBB|CCC中的一种,但不知道具体是哪一种,于是通过特定的条件判断,去确定在场景一A一定是AAA类型,场景二一定是BBB类型的函数或者语句。
interface AAA{
aaa:number,
ttt:number,
}
interface BBB{
bbb:number,
ttt:number,
}
interface CCC{
ccc:number,
ttt:number,
}
type TYPE = AAA|BBB|CCC
const isString = (x: unknown): x is string => {
return typeof x === 'string';
};
const isAAA = (A:TYPE): A is AAA=>{
return !!A && 'aaa' in A
}
const isAllowedIconKey = (x: string): x is VIconsKey => {
return ['HomeOutline', 'OptionsOutline', 'UserOutline'].includes(x);
};
keyof typeof
keyof 是 TypeScript 特有的操作符,作用是提取一个类型的所有键名,返回这些键名组成的字符串 / 数字联合类型
typeof 是判断值的类型(返回 "string"/"number" 等),而 TypeScript 中的 typeof 是获取一个值的完整类型,需要绑定as const做一个类型约束
当你需要提取类的key作为type约束
export const vIcons = {
NewspaperOutline: NewspaperOutline,
FolderOutline: FolderOutline,
OptionsOutline: OptionsOutline,
} as const; // 一定需要加as const 做一个类型约束
// 🌟 提取 vIcons 的键名类型(核心语法)
type VIconsKey = keyof typeof vIcons;
//本质上上面等于下面
type VIconsKey = 'NewspaperOutline'| 'FolderOutline' | 'OptionsOutline'
never
常用于错误兜底的场景,比如非正常的枚举分支/try catch错误捕获后的分支处理,提高项目健壮性
enum MenuStatus {
Normal = 'normal',
Disabled = 'disabled',
Hidden = 'hidden'
}
// 🌟 核心:兜底函数,接收 never 类型(只能传入不可能的类型)
function assertNever(value: never): never {
throw new Error(`未处理的类型:${value}`);
}
// 处理菜单状态(确保覆盖所有枚举值)
function handleMenuStatus(status: MenuStatus): string {
switch (status) {
case MenuStatus.Normal:
return '正常显示';
case MenuStatus.Disabled:
return '禁用状态';
case MenuStatus.Hidden:
return '隐藏状态';
default:
return assertNever(status);
}
}
type,interface和元组
- type: 给任意类型起别名,复用复杂类型
//联合类型
type abc = string|number
//交叉类型
type abc2 = BaseMenu & Menu
//元组
type abc3 = [number, string, number]
- interface:常用于接口入参,支持extends扩展和implements实现
// 示例1:基础接口(菜单配置,项目中最常用)
interface Menu {
label: string;
path: string;
icon?: string;
children?: Menu[]; // 嵌套自身,描述子菜单
}
// 示例2:接口扩展(继承其他接口,比type交叉类型更语义化)
interface MenuWithAuth extends Menu {
permissions: string[]; // 新增权限字段
}
const menu: MenuWithAuth = {
label: '首页',
path: '/dashboard',
permissions: ['read', 'write'],
};
// 示例3:类实现接口(面向对象场景)
interface Printable {
print: () => void;
}
class MenuService implements Printable {
print() {
console.log('打印菜单配置');
}
}
Typescript的数据类型
- 基础数据类型
string,number,undefined,null,symbol,boolean,bigint
- 复合类型
array,tuple,object,enum
tuple是特殊的,固定长度固定类型的数组,比如 const arr:[number,string,number] = [1,'2',3]
enum是命名常量,常用可枚举的类型限制
- 特殊类型
any,unknown,never,void
any 不安全类型
unknown推荐的类型限制,不清楚具体类型,于是在实际的代码执行层再进行类型限制
never 永远不会执行的类型,一般用于错误捕获
void 函数无返回值
- 高级类型
联合类型,交叉类型,类型断言
联合类型: let str:number | number
交叉类型: type strname = Name & Age
类型断言: let val: unknown = "hello";const str = val as string;
ts中的satisfies有什么用
声明一个变量满足一种类型的类型约束,同时保留这个变量推断出来的自身的特性
// 定义一个类型:颜色可以是字符串,或 RGB 数字数组
type Color = string | [number, number, number];
// 问题1:用 : 注解类型 → 约束了类型,但丢失具体字面量类型
const red: Color = '#ff0000';
// ❌ TS 无法推断 red 是具体的 '#ff0000',只能推断为 Color(string | [number,number,number])
// 比如想访问 red.length 会报错:因为 Color 可能是数组,TS 不确定
// red.length; // 报错:Property 'length' does not exist on type 'Color'
// 问题2:不用 : 注解 → 保留具体类型,但无法约束结构
const green = [0, 255, 0]; // TS 推断为 number[],而非 [number, number, number]
// ❌ 可以随意修改数组长度,破坏约束
green.push(100); // 数组变成 [0,255,0,100],不符合 Color 类型,但 TS 不报错
type Color2 = string | [number, number, number];
// ✅ satisfies 做两件事:
// 1. 校验 '#ff0000' 符合 Color2 类型(约束);
// 2. 保留 red2 的具体类型为 '#ff0000'(而非 Color2)。
const red2 = '#ff0000' satisfies Color2;
// ✅ TS 知道 red2 是具体的字符串 '#ff0000',可以安全访问 length
console.log(red2.length); // 输出 7
// ✅ 校验 [0,255,0] 符合 Color2 类型,且保留具体类型为 [0,255,0](元组)
const green2 = [0, 255, 0] satisfies Color2;
// ❌ 尝试修改数组长度会报错:因为 green2 是具体的元组 [0,255,0],长度固定
// green2.push(100); // 报错:Argument of type '100' is not assignable to parameter of type 'never'
// ❌ 如果值不符合 Color 类型,satisfies 会直接报错
// const blue2 = [0, 0] satisfies Color2; // 报错:长度为2的数组不符合 Color2 约束
// 定义配置类型:theme 只能是 light/dark,size 只能是 sm/md/lg
type ComponentConfig = {
theme: 'light' | 'dark';
size: 'sm' | 'md' | 'lg';
disabled?: boolean;
};
// ✅ satisfies 约束对象符合 ComponentConfig,同时保留每个值的具体类型
const buttonConfig = {
theme: 'dark',
size: 'md',
disabled: false
} satisfies ComponentConfig;
// ✅ TS 知道 buttonConfig.theme 是具体的 'dark',而非 'light'|'dark'
if (buttonConfig.theme === 'dark') {
console.log('暗黑模式'); // 类型安全,无报错
}
// ❌ 如果对象结构不符合,会报错
// const inputConfig = {
// theme: 'red', // 报错:'red' 不符合 'light'|'dark'
// size: 'md'
// } satisfies ComponentConfig;
ts 的 enum、const enum 的区别
- enum 编译后会生成一个索引对象,所以可以支持反向取值
// 编译前
enum Status {
Success = 200,
Error = 500,
NotFound = 404
}
const code = Status.Success;
console.log(Status.Error);
// 编译后
var Status;
(function (Status) {
Status[Status["Success"] = 200] = "Success";
Status[Status["Error"] = 500] = "Error";
Status[Status["NotFound"] = 404] = "NotFound";
})(Status || (Status = {}));
var code = Status.Success;
console.log(Status.Error);
//因此可以反向取值,当做对象索引使用
function getStatusText(code: number): string {
return StatusCode[code] || 'Unknown';
}
console.log(getStatusText(200));
- const enum编译后会在使用的地方自动替换成枚举之后的数据
//编译前
const enum Status {
Success = 200,
Error = 500,
NotFound = 404
}
const code = Status.Success;
console.log(Status.Error);
//编译后,不会生成枚举对象,无法反向索引,但是编译出来的结果更加轻量
var code = 200 /* Success */;
console.log(500 /* Error */);