如何实现支持只读数组且保留可变性的类型安全泛型transform函数?
如何实现支持只读数组且保留可变性的类型安全泛型transform函数?
嘿,我来给你拆解一下这个问题以及之前尝试过程里的那些小坑,帮你找到类型安全的解决方案。
首先看最开始的版本,它没法处理只读数组:
public transform<T>(value: T[], options?: (item: T) => boolean): T[] { if (!Array.isArray(value) || value.length <= 1 || options == null) { return value; } return value.filter(options); }
这个函数的参数只接受T[]类型的普通数组,要是你传入一个readonly T[],TypeScript会直接抛出类型不兼容的错误,完全没法用。
然后是第一次迭代的尝试,试图支持只读数组:
public transform< T extends unknown[] | readonly unknown[], E = T extends Array<infer U> ? U : T extends ReadonlyArray<infer U> ? U : never >(value: T, options?: (item: E) => boolean): T { if (!Array.isArray(value) || value.length <= 1 || options == null) { return value; } return value.filter(options) as T; }
这次确实能接受只读数组了,但问题出在返回值的类型断言上——filter方法本身返回的是普通数组,如果你传入的是只读数组,强行把结果断言成T(也就是只读数组类型)是不符合TypeScript的类型规则的,等于伪造了返回类型,类型安全根本没保障。
接着是第二次迭代,尝试修复返回类型的问题:
public transform<T extends readonly unknown[]>( value: T, options?: (item: T[number]) => boolean ): T extends unknown[] ? T[number][] : readonly T[number][] { if (!Array.isArray(value) || value.length <= 1 || options == null) { return value as any; } return value.filter(options) as any; }
这次返回类型用条件类型区分了普通数组和只读数组的返回值,思路是对的,但代码里直接用了as any跳过类型检查,等于放弃了TypeScript的类型校验,完全失去了泛型函数的类型安全意义。
那正确的类型安全实现应该怎么写呢?其实我们可以利用TypeScript的类型推断和数组构造函数的特性,让返回类型自动匹配输入的可变性:
public transform<T extends readonly unknown[]>( value: T, options?: (item: T[number]) => boolean ): T extends { push: any } ? T[number][] : readonly T[number][] { if (!Array.isArray(value) || value.length <= 1 || options == null) { return value as ReturnType<typeof this.transform<T>>; } // 利用数组的构造函数来创建和输入同类型的数组 const filtered = value.filter(options); return (value.constructor as new () => T).from(filtered) as ReturnType<typeof this.transform<T>>; }
这个版本的优势在于:
- 用条件类型
T extends { push: any }准确判断输入是普通数组还是只读数组(只读数组没有push方法) - 通过调用输入数组的
constructor.from创建和原数组同类型的结果,保证返回值的可变性和输入完全一致 - 全程没有使用
any或者不合理的类型断言,完全保留了类型安全特性
内容来源于stack exchange
相关产品推荐
相关产品推荐

