首页 > 解决方案 > 从类工厂函数创建清晰的 TypeScript 类

问题描述

我有一个第三方库,它返回OpaqueObject包含对打字稿不透明的字段的对象(类型)。对于任何给定OpaqueObject的,我知道定义和/或需要哪些字段,但 Typescript 一无所知。给定的“模式”OpaqueObject简单地列出了定义的字段,并带有一个布尔标志,指示它们是否需要存在:

type OpaqueObjectSchema = { [field: string]: boolean }

字段是OpaqueObject通过getField函数检索的。getField使用类型谓词来调整返回类型,具体取决于是否需要该字段。获取不存在的必填字段会引发错误。这是签名:

getField<B extends boolean>(
  obj: OpaqueObject, field: string, isRequired: B,
): B extends true ? string : (string | undefined)

我有一个类工厂函数 ( opaqueObjectWrapperFactory),它接收OpaqueObjectSchema并输出一个包装类。例如,我有一种类型,OpaqueObject它有两个字段:f1f2. f1是必须的; f2可能未定义。我工厂生产的类的硬编码版本是:

class OpaqueObjectWrapper {

  private obj: OpaqueObject

  constructor(obj: OpaqueObject) {
    this.obj = obj;
  }

  get f1(): string { return getField(this.obj, 'f1', true) }
  get f2(): string | undefined { return getField(this.obj, 'f2', false) }
}

// OpaqueObjectWrapper is equivalent to the return value of:
opaqueObjectWrapperFactory({
  f1: true,
  f2: false,
});

我的问题是我无法弄清楚如何使这些生成的类对 Typescript 清晰可见。工厂应该像这样工作:

// this declaration should have the same effect as hardcoding `OpaqueObjectWrapper`
class OpaqueObjectWrapper extends opaqueObjectWrapperFactory({ f1: true, f2: false });

显然我需要以某种方式使用泛型,但我不确定如何从输入模式派生返回接口。这可能吗?

标签: typescript

解决方案


派生对象类型

AnOpaqueObject是所有值string都是必需的,而其他键是可选的。让我们首先定义一个类型,假设我们已经知道哪个是哪个:

type OpaqueObject<AllKeys extends string, RequiredKeys extends string> = Partial<Record<AllKeys, string>> & Record<RequiredKeys, string>

如果我们想从一个模式转到一个对象,我们知道我们需要找到所有的键——这很容易keyof Schema——以及所需的键。如果模式中的值是 ,我们知道如果需要的键是true.

重要提示:我们必须as const在创建模式时使用才能与 分开truefalse否则我们只知道我们有一个boolean.

架构所需的键S是:

type RequiredKeys<S> = {
    [K in keyof S]: S[K] extends true ? K : never;
}[keyof S]

所以我们现在可以为一个OpaqueObject只依赖于模式的类型写一个类型S

type OpaqueObject<S> = Partial<Record<keyof S, string>> & Record<RequiredKeys<S>, string>

获取字段

现在到getField功能。我们不想传入一个 boolean required 标志,因为我们应该已经知道这一点。相反,让我们让它依赖于泛型 schemaS和 key K

function getField<S, K extends keyof S>(
    obj: OpaqueObject<S>, field: K
): OpaqueObject<S>[K] {
    return obj[field];
}

但老实说,如果我们有一个正确类型的对象,那么整个函数就变得不必要了,因为我们可以通过直接访问属性获得正确的返回类型。

const exampleSchema = {
    f1: true,
    f2: false,
} as const;

type ExampleObject = OpaqueObject<typeof exampleSchema>

class OpaqueObjectWrapper {

  private obj: ExampleObject

  constructor(obj: ExampleObject) {
    this.obj = obj;
  }

  get f1(): string { return this.obj.f1 }
  get f2(): string | undefined { return this.obj.f2 }
}

未知数

关于是否可能,我对模式的来源感到困惑as const。这些是来自外部来源的变量吗?还是您通过在代码中写出对象来定义它们?

让我对您的问题感到困惑的部分是在课堂get f1()get f2()以动态的方式实施。与 PHP 等其他语言不同,Javascript 没有属性值未知的动态 getter。您只能通过Proxy.

代理对象

我知道如何动态获取属性的唯一方法是使用Proxy。我已经解决了这个问题。我仍然缺少的部分是如何在代理的伪类上实现构造签名。

这个代理接受一个类的实例,该实例将对象存储为属性obj,并允许我们直接访问属性obj。为了让 typescript 理解添加的属性,我们必须断言可以访问as Constructable<T> & T的所有属性。T

const proxied = <T,>(inst: Constructable<T> ) => {
    return new Proxy( inst, {
        get: function <K extends keyof T>(oTarget: Constructable<T>, sKey: K): T[K] {
            return oTarget.obj[sKey];
        },
    }) as Constructable<T> & T
}

我用来存储对象的底层类是

// stores an object internally, but allows it to be created by calling new()
class Constructable<T> {

    private _obj: T

    constructor(obj: T) {
        this._obj = obj;
    }

    // object is readonly
    get obj(): T {
        return this._obj;
    }
}

所以现在我们要创建一个基于模式的代理类。我们想要这个:

interface ProxiedConstructable<T> {
    // pass in an object T and get something which can access of the properties of T
    new( args: T ): Readonly<T>;
}

我告诉过你我并不是一路走来,这是因为代理适用于类的实例而不是类本身,所以我坚持让我们的工厂返回一些“新的”,但这就是我有:

const makeProxied = <S extends { [field: string]: boolean }>(schema: S) => 
    (obj: OpaqueObject<S>) => {
        return proxied( new Constructable(obj) );
    }

像这样工作:

const test = makeProxied({
    f1: true,
    f2: false
} as const);

const testObj = test({f1: "hello world"});

const f1: string = testObj.f1;

const f2: string | undefined = testObj.f2;

游乐场链接


推荐阅读