likes
comments
collection
share

如何正确用ts声明组件(函数)的props(入参),看这篇就够了

作者站长头像
站长
· 阅读数 15

首先要说明的是,这篇文章不是教你如何封装一个组件。如果想要学习此类问题,我推荐阅读下面两篇文章:

如何开发一个人人爱的组件? 这一年我的对组件的思考

我们该如何编写声明文件,才能达到直接面向接口开发。

从而满足简单、方便、正确的使用组件并获取近乎完美的的开发体验呢?

一、明确区分必填和非必填

这一条很基础也很重要。好的组件应该是尽可能少的必填属性。甚至不需要传递任何参数

在顺序上,一般必填的的属性在前面,方便一目了然的阅读

export interface CmpProps {
  // 必填
  id: string;

  // 选填
  code?: number
}

二、简洁但丰富的注释

对于简单属性,注释不需要过多,清晰明了即可

如果对于一些复杂的属性,除了文字描述外,可以适当的补充使用示例

存在默认值也要注明默认值

export interface CmpProps {
  /**
   * 组件id
   */
  id: string;

  /**
   * 提示信息
   * defaultValue: '请输入'
   */
  placeholder?: string;

  /**
   * 获取编码,根据不同参数类型返回不同结果
   * getCode(1) -> '1'
   * getCode('1') -> 1
   */
  getCode?: (a: number) => string
  getCode?: (a: string) => number
}

三、完整且正确的类型描述

对于每一个属性的类型,要描述完整。

3.1 多case函数

例如事件监听函数,我们可以使用函数重载来描述每一个case的输入输出关系

export interface EventProps {

  /**
   * 监听指定的事件
   * onStart: 普通回调
   * beforeClose: 回调函数返回reject,将会阻止关闭。
   */
  on?: (eventName: 'onStart', callback: () => void) => void
  on?: (eventName: 'beforeClose', callback: () => Promise<string>) => void
}

3.2 原生属性、框架内置属性

这部分就需要大家没事多看看定义

export interface EventProps {

  /**
   * 样式
   */
  style?: React.CSSProperties;

  /**
   * 点击事件
   */
  onClick?: React.MouseEventHandler<HTMLDivElement>;

  /**
   * 显示标签
   * React.ReactNode 和 React.ReactElement 的区别你搞清楚了吗
   */
  label: React.ReactNode;
}

3.3 最小范围

顾名思义,不要扩大类型的定义。比如值是一个确切的字符串,而不是 string

当然,如果是多个值,最好使用枚举单独定义并导出


export enum SizeType = {
  // 小
  small,
  // 中
  middle,
  // 大
  large,
}

export interface EventProps {

  /**
   * 名称
   */
  name?: 'haoza' 

  /**
   * 尺寸
   * defaultValue: SizeType.small
   */
  size?: SizeType
}

3.4 使用函数属性而不是函数方法

原因见我之前的 文章

四、正确的类型检查

这部分相对而已比较复杂,要做到正确的类型检查并不容易。我会通过以下几个 case 来展示

4.1 输入输出

这是相对简单的示例,组件的多个属性值由运行时确认

export interface CmpProps<T> {
  value: T;
  // 回调的参数类型和输入的 value 保持一致
  onChange?: (val: T) => void
}

4.2 固定格式

比如某些数据埋点的属性需要以固定的格式传递


export type CmpProps = {
  name: string;
  code: number;
} & Record<`data-${string}`, string>;

4.3 属性互斥

比如当你传入属性 A 的时候,就希望不要传入属性 B,反之亦然。

最简单的方案是手动写联合类型

export interface CmpProps =
| { id: string, name?: never }
| { name: string, id?: never }

当然,作为工程师。我们肯定不会使用如此丑陋的方式。利用体操函数可以简化此过程

来源

/**
 * 多个属性相互互斥
 */
export type JustOne<T, K extends (keyof T)[] = [], Y extends keyof T = K[number]> = NonNullable<
  {
    [x in Y]: Pick<T, Exclude<keyof T, Exclude<Y, x>>> & SetKeyNever<T, Exclude<Y, x>>;
  }[Y]
>;

type props = { name: string, id: string }

export type CmpProps = JustOne<props, ['name', 'id']>,

4.4 属性对(组)

顾名思义,这些属性要成对出现使用。

此处有两种方案:

  • 将对应属性归纳到一个组,再放在某个属性下
  • 属性组平级,使用联合类型

假如 value 和 defaultValue 都是非必填,但是其中一个被使用,另一个也要跟着使用

// 方案1
export type CmpProps<T>  = {
  input?: {
    value: T;
    defaultValue: T;
  }

  onChange(val: T): void
} 

// 方案2
export type CmpProps<T>  = {
  value: T;
  defaultValue: T;

  onChange(val: T): void
} | 
{
  value?: never;
  defaultValue?: never;

  onChange(val: T): void
} 

当然方案2也可以用体操函数帮助我们节省时间,请参考上面互斥自行实现哦~

4.5 属性类型联动可变

一个常见的场景就是:下拉框默认是单选。如果传入 multiple 为 true,则变成多选。


type SelectComboValue<T, M> = M extends false | undefined ? T : T[];

export type CmpProps<T, M extends boolean = false> = {
  // 是否多选
  multiple?: M;

  value: SelectComboValue<T, M>;

  onChange?(val: SelectComboValue<T, M>): void;
};


function Test<T, M extends boolean = false>(props: CmpProps<T, M>) {}


// 会正确推断 value 应该传入数组。 onChange的参数是一个数组
Test({
  value: [],
  multiple: true,
  onChange(val) {
    val.concat([]);
  },
});

Test({
  value: 1,
  multiple: false,
  onChange(val) {
    val.toFixed()
  },
});

当然还有其他类似场景,比如传入一个对象。另一个属性的值被限定为该对象的属性path

比如 value: { user: { name: 'haoza', age: 18 } }

另一个属性 disabledPaths 的可选值为 user.name | user.age 而不是 string[]

4.6 禁止合并 anyObject

在我们定义入参接口的时候,一定是有边界的,键值对可枚举的!

不能使用 object, { [k: stirng]: any }, Record<any, any> 等方式定义

比如以下都是错误方式!

export type CmpProps<T>  = {
  name: string,
  id: numnber,
  // ❌ 千万不要这样定义
  [k: string]: any
} 

export type CmpProps<T>  = {
  name: string,
  id: numnber,

  // ❌ 千万不要这样定义
} & Record<string, any>

export type CmpProps<T>  = {
  name: string,
  id: numnber,

  // ❌ 千万不要这样定义
} & object

当然,更加不要直接使用 any,否则你前面所有的入参定义都白费了。

呜呼 楚人一炬,可怜焦土!

总结

如果严格按照以上的规则来定义声明文件,你的组件一定是熠熠生辉的。

所以拒绝无脑 any ,从我做起:)