如何在不生成运行时代码的情况下创建匹配类实例解构结果的TypeScript类型?
我理解你的需求:你想得到类实例通过对象解构后的类型,同时避免在运行时创建多余的实例变量,还希望保留私有属性(尽管运行时解构其实无法获取私有属性)。下面分场景给出针对性解决方案:
场景1:不需要保留私有属性(匹配真实运行时行为)
如果你只需要和const dummy = {...new Foo()}完全一致的类型,但不想有运行时变量,可以利用**类型声明(declare)**来模拟这个类型推导——declare变量仅存在于编译时,不会生成任何运行时代码:
class Foo { a?: number; private b?: string; c(): void { } d?: () => void; get e(): number; }; // 仅编译时存在,不会出现在最终JS文件中 declare const dummy: typeof {...new Foo()}; type Target = typeof dummy; // TypeScript推导结果:type Target = { a?: number; d?: () => void; }
这个方案完全复用了TypeScript对解构语法的内置类型推导逻辑,和你之前用dummy变量得到的类型完全一致,且没有任何运行时开销。
场景2:希望保留私有属性(类型层面,忽略运行时限制)
如果你确实需要在类型中包含私有属性(即使运行时解构无法获取),需要注意:私有属性仅能在类内部被合法引用,所以这个方案只能在类内部生效:
class Foo { a?: number; private b?: string; c(): void { } d?: () => void; get e(): number; // 在类内部定义目标类型 type Target = typeof {...new Foo()} & Pick<this, 'b'>; } // Target类型等价于你期望的:{ a?: number; b?: string; d?: () => void; }
如果必须在类外部使用包含私有属性的类型,你需要将私有属性改为受保护(protected)或公共属性,或者通过类的静态方法暴露这个类型——因为TypeScript在类外部不允许引用私有属性的键名。
为什么不用映射类型直接实现?
你提到之前考虑过用映射类型过滤函数,但会错误移除d这类函数类型的实例属性。这是因为TypeScript的类型系统无法直接区分类方法(原型上的函数,带this参数)和实例属性函数(自有属性,不带this参数),不过我们可以通过this参数做区分:
// 区分类方法(带this参数)和实例属性函数 type IsMethod<F> = F extends (this: any, ...args: any[]) => any ? true : false; // 生成仅包含实例属性(包括函数类型属性)的类型 type InstanceProperties<T> = { [K in keyof T as IsMethod<T[K]> extends true ? never : K]: T[K] }; class Foo { a?: number; private b?: string; c(): void { } d?: () => void; get e(): number { return 0; } }; type Target = InstanceProperties<Foo>; // Target类型为:{ a?: number; b?: string; d?: () => void; e: number; }
但这个方案会保留getter属性e(因为TypeScript类型系统无法区分getter和普通属性),这和你通过解构得到的类型不一致——如果你需要完全匹配解构的类型,还是推荐用declare的方案,因为它直接复用了TypeScript对解构语法的内置推导逻辑。
总结
- 若要完全匹配解构的真实类型且无运行时开销:使用
declare变量模拟推导。 - 若要在类型中包含私有属性:在类内部结合
typeof {...new Foo()}和Pick实现。 - 若可以接受保留getter属性:使用基于
this参数区分方法的映射类型。
内容来源于stack exchange

