首页 > 解决方案 > 高级 TS 类型:镜像键但成为新值的包装器类型

问题描述

我正在尝试编写一个需要一些接口的类型定义,如下所示:

interface IMyInterface {
   name: string
   subobject: {
     boolField: boolean
   }
}

然后OptionsTransformer将其包装起来,对于每一key-value对,保持密钥相同,但将值更改为新的 class FieldOptions<type>。例如:

const transformed: OptionsTransformer<IMyInterface> = {
     name: new FieldOptions<string>(),
     subobject: {
         boolField: new FieldOptions<boolean>()
     }
}

这是因为我知道TS接口在运行时会消失,因此您无法在运行时真正检查它们。我希望能够编写如下函数(我的实际用例是在 post 请求的正文中输入:我知道它应该是什么样子,并想验证它看起来像那样):

const runtimeVerifier<T> = (
   options: OptionsTransformer<T>, input: Record<string, unknown>
) => {
   //for each value in input, run the it's verifier. If it's a sub-object,
   //recurse and do runtime verifier on the sub-object.
}

另一个要求是,在编译时,如果我向 中添加一个字段IMyInterface,我希望TS编译器在那里给我一个错误,例如“您的 optionsTransformer 缺少该字段。”。

我对这个定义已经很远了:

//this class doesn't matter, but here for completeness
class FieldOption<T> {}

export interface IValidBody {
    [x: string]: boolean | string | number | IValidBody 
}

export type OptionsTransformer<T extends IValidBody> = {
    [key in keyof T]-?: T[key] extends IValidBody 
        ? IOptionsForFields<T[key]> 
        : FieldOption<T[key]>
}

但是我在子对象上遇到了一些非常复杂的错误,并且无法破译它们。

任何帮助,将不胜感激。

标签: typescripttypescript-typingstypescript-generics

解决方案


经过几天的黑客攻击,我找到了解决方案。

我非常接近 - 让泛型OptionsTransformer强制递归子类型也实现接口 - 即使它做到了,类型也会被删除(出于我不知道的原因)然后它没有实现类型。

解决方案是让OptionsTransformer泛型不扩展子类型。

最终的解决方案如下所示,为清晰起见重命名了类:

//can be any class!
class AnyClass<T> {
    constructor(item: T) {
        console.log(typeof item)
    }
}

//just using string here, but can be any type(s).
interface RecursiveInterface {
    [key: string]: string | RecursiveInterface
}

//fix is here: T no longer extends Recursive interface. 
//I'm not 100% sure why, but it works. Any sub object will be 
//"duck typed" into recursive interface, so you can nest as deeply
//as you desire. 
type RecursiveInterfaceWrappedWithClass<T> = {
    [key in keyof T]: T[key] extends RecursiveInterface ? RecursiveInterfaceWrappedWithClass<T[key]> : AnyClass<T[key]>
}


//example: interface with nested object. Only go 1 deep in this example,
//but you can nest as far as you'd like.
interface MyObject {
    foo: string
    bar: {
        baz: string
    }
}


//here's the wrapper.
const myObjectWrapped: RecursiveInterfaceWrappedWithClass<MyObject> = {
    foo: new AnyClass('string'),
    bar: {
        baz: new AnyClass('hi')
    }
}

推荐阅读