typescript 在抽象类示例之间维护类型安全

6bc51xsx  于 2022-12-01  发布在  TypeScript
关注(0)|答案(1)|浏览(118)

我需要确保以下泛型抽象类的类型安全的帮助,该类使用代理:

代理.ts

function authHandler<
  F extends Families,
  T extends controllerAbstract<F>
>(): any {
  return {
    get: function (target: T, prop: keyof T): any {
      return (...args: any[]) => {
        target.authorize((controllerClient: Client<F>) => {
            
          return target[prop](controllerClient, args);
        });
      };
    },
  };
}

控制器摘要.ts

abstract class controllerAbstract<F extends Families> {
  controllerFamily: F;

  constructor(controllerFamily: F) {
    this.controllerFamily = controllerFamily;
    return new Proxy(this, authHandler<F, typeof this>());
  }

  abstract build_client(): Client<F>;
   
  authorize(consume_client: (client: Client<F>) => typeof this[keyof typeof this]) {
    consume_client(this.build_client());
  }
}

混凝土.ts

const controllerFamilyName = 'FamilyA';
type ControllerFamliy = typeof controllerFamilyName;

class FamilyAController extends controllerAbstract<ControllerFamliy> {
  constructor() {
    super(controllerFamilyName);
}

  build_client(): Client<ControllerFamliy> {
    // build a client of type "FamilyA"
    const client = { id: 'A' } as Client<ControllerFamliy>;
    return client; 
  }

  // proxy "authorize" provides client object
  useClient(client: any, args: any[]) {
    // i want client to implicitly be of type "FamilyAClient_t" / Client<"FamilyA">
    // currently just using "any" as placeholder
  }
}

类型.ts**

type FamilyAClient_t = { id: 'A' };

type FamilyBClient_t = { id: 'B' };

interface ClientTable {
  FamilyA: FamilyAClient_t;
  FamilyB: FamilyBClient_t;
}

type Families = keyof ClientTable;

type Client<F extends Families> = ClientTable[F];

注意:这里我说的“concrete”类,我指的是concrete.ts的内容,也就是说,它是一个扩展抽象类的类,FamilyAController只是一个例子。
我的目标是使concrete.tsFamilyAControlleruseClient方法的client参数反映基于具体类的controllerFamilyName常量的正确类型,这样我就可以调用client.id,并且如果controllerFamilyName == 'FamilyA',则得到正确的自动完成“A”,如果controllerFamilyName == 'FamilyB',则得到“B”。
基于here解决方案,我已经更改了代理中的显式类型,以尝试获得所需的识别类型。这就是我认为类型安全在我的实现中出现故障的地方。
然而,这仍然是错误的,因为:

  • 代理target[prop](...)行给出了一个错误“this expression is not callable”,因为target[prop]的类型是unknown。2这是因为并非target的每个属性都是可调用的,但有些属性是可调用的,我希望能够将target的类型限制在这个子集。
  • 我尝试为抽象类authorize方法的consume_client函数参数定义返回类型:authorize(consume_client: (client: Client<F>) => typeof this[keyof typeof this]) .会掷回错误。目的是让它传回在concrete中定义之任何方法传回型别的型别。例如,如果另一个方法存在于concrete类别上,那么消费客户端返回类型应该能够自动处理这个新的方法返回类型。代理层维护基于具体类的 * 任何 * 可调用方法而存在的相同返回类型。

另一个尝试过的方法是定义另一个抽象类,其中只包含与target[props](controllerClient)签名一致的抽象方法,并使用keyof这个类作为道具类型,以修复第一个错误。这是成功的,但我仍然不确定如何达到我的最终目标。
本质上我想要的是,在 concrete.ts 中,能够写useClient(client),并且客户端类型隐式地是Client<ControllerFamily>,这样我就不必为每个具体类变体的每个方法写useClient(client: Client<ControllerFamily>)。这可能吗?
希望这有助于澄清我编写代码的目标,以及我目前的进度。

icnyk63a

icnyk63a1#

不幸的是,TypeScript目前无法赋予继承自超类的类方法参数“隐式”或“上下文”类型。您需要手动注解所有方法参数,然后可以针对超类 * 检查 * 这些参数。所以如果你做错了,你会得到一个错误。但是你仍然必须手动操作。这绝对是令人讨厌的。已经有很多功能要求改进它(例如,请参见microsoft/TypeScript#23911),但这些都没有进入语言。似乎总是有一个性能问题,或者破坏现实世界的代码。目前,你能做的最好的事情将是手动注解。
话虽如此,我能得到的最接近你想要的是尝试告诉编译器ControllerAbstract实际做什么......也就是说,任何添加的方法都必须接受两个参数,第一个参数是Client<F>类型,对应于相关的generic类型参数F
我说“尝试”是因为没有完美的方法来完成这个任务,没有直接的支持来描述“除了已知键之外的所有string”,所以没有办法说“ControllerAbstract<F>上除了"controllerFamily""buildClient""authorize"“。在microsoft/TypeScript#17867上有一个长期的开放特性请求,但谁知道它何时或是否会被实现。目前只有解决方案(请参见How to define Typescript type as a dictionary of strings but with one numeric "id" property以获得这些解决方案的列表)。
一种解决方法是将对象的已知部分与“其他所有内容”部分的索引签名相交。

abstract class _ControllerAbstract<F extends Families> {
  // ✂ snip ✂
}

type ControllerAbstract<F extends Families> = _ControllerAbstract<F> &
  { [k: string]: (client: Client<F>, args: any[]) => any }

但这是不真实的;这意味着,比如说,buildClient属性将 * 既是 * () => Client<F>类型的方法 * 又是 * (client: Client<F>, args: any[]) => any类型的方法。编译器知道这不是真的,所以你不能直接把索引签名添加到类体中。相反,你需要做我上面做的事情:重命名实际的类体,并使用类型Assert来使编译器相信ControllerAbstract是一个行为符合要求的构造函数:

const ControllerAbstract = _ControllerAbstract as abstract new
  <F extends Families>(controllerFamily: F) => ControllerAbstract<F>;

好吧,让我们看看它是否有效:

const controllerFamilyName = 'FamilyA';
type ControllerFamily = typeof controllerFamilyName;    
class FamilyAController extends ControllerAbstract<ControllerFamily> {
  constructor() {
    super(controllerFamilyName);
  }

  build_client() {
    return { id: "A" } as const;
  }

  useClient(client: Client<ControllerFamily>, args: any[]) { }

  useClientBad(client: Client<"FamilyB">, args: any[]) { }
  // Property 'useClientBad' of type '(client: FamilyBClient_t, args: any[]) => void' is
  // not assignable to 'string' index type '(client: FamilyAClient_t, args: any[]) => any'.(2411)
}

看起来不错。useClient()方法类型检查,而useClientBad()方法不检查,并且那里的错误描述了问题,clientFamilyBClient_t,而它应该是FamilyAClient_t
所以,你去那里。它不是完美的,但它是我能得到的最接近。
Playground代码链接

相关问题