TypeScript使用经验总结

69 阅读7分钟

最近在把负责的项目从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一般应用的场景都在:

  1. 老项目迁移过渡用,方便快速转成ts,再支持后续迭代
  2. 第三方项目,不太清楚实际的内部结构,比如这次我的项目接的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的数据类型

  1. 基础数据类型

string,number,undefined,null,symbol,boolean,bigint

  1. 复合类型

array,tuple,object,enum

tuple是特殊的,固定长度固定类型的数组,比如 const arr:[number,string,number] = [1,'2',3]

enum是命名常量,常用可枚举的类型限制

  1. 特殊类型

any,unknown,never,void

any 不安全类型

unknown推荐的类型限制,不清楚具体类型,于是在实际的代码执行层再进行类型限制

never 永远不会执行的类型,一般用于错误捕获

void 函数无返回值

  1. 高级类型

联合类型,交叉类型,类型断言

联合类型: 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 的区别

  1. 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));
  1. 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 */);