首页 > 解决方案 > TypeScript:强制转换整个类类型

问题描述

我想知道是否可以更改类属性的类型?但不是简单的强制转换,而是覆盖整个类类型。

假设我有以下课程:

class Test {
  public myNumber = 7;
}

是否可以将myNumber属性的类型从number更改为string

假设我们有一个自定义的 typescript 转换器,它将类的每个 number 类型的属性转换为字符串。那么在开发过程中以某种方式反映这一点会很酷。这就是为什么我问是否可以调整类型。

我正在寻找一个选项来覆盖整个类类型定义而不强制转换每个属性。例如不这样做:

const test = new Test();
(
test.myNumber as string).toUpperCase();

我的第一个想法是这可以通过索引类型来实现。但我想问是否有人已经有这方面的经验或有具体的想法。

例如,调用一个函数会很酷......

whatever(Test)

...在此之后,类的类型发生了变化。所以编译器应该知道从现在开始,例如myNumber应该是string而不是number类型。

所以现在应该可以了:

const test = new Test();
test.myNumber.toUpperCase();

这个例子的意义并不重要。这只是一个虚构的用例,以(希望)简单的方式说明问题。

===

因为在这个问题的评论中提到了上下文(如何使用该类),所以我想另外提供以下示例。它是使用jasminekarma的Angular组件的测试(规范)文件。我试图用代码片段中的注释来解释自己。

describe('ParentComponent', () => {
  let component: ParentComponent;
  let fixture: ComponentFixture<ParentComponent>;

  /**
   * This function is the "marker" for the custom typescript transformer.
   * With this line the typescript transformer "knows" that it has to adjust the class ParentComponent.
   *
   * I don't know if this is possible but it would be cool if after the function "ttransformer" the type for ParentComponent would be adjusted.
   * With adjusted I mean that e.g. each class property of type number is now reflected as type string (as explained in the issue description above)
   */
  ttransformer(ParentComponent);

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ ParentComponent ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(ParentComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('test it', () => {
    // This cast should not be necessary
    (component.ttransformerTest as jasmine.Spy).and.returnValue('Transformer Test');
    expect(component.ttransformerTest).toHaveBeenCalled();
  });
});

===

在此先感谢,卢卡斯

标签: typescripttypescript-typingstypescript-compiler-api

解决方案


这是一种奇怪的模式,而不是我建议轻而易举的事情,但它在很大程度上是可以实现的:

实例和构造函数

当你声明你的 classTest时,Typescript 确实做了两件事:一个名为的类型Test,它是一个Test类的实例,一个名为的值Test,它是一个构建该类型的构造函数Test。虽然 TS 能够给它们赋予相同的名称,并通过一个class声明将它们带入范围,但我们必须单独处理它们。

转换实例

因此,处理实例时,我们可以编写一个类型,将Test带有数字的实例转换为带有字符串的实例:

type NumbersToStrings<T> = {
    [K in keyof T]: T[K] extends number ? string : T[K]
}
type TransformedTest = NumersToStrings<Test>; // Remember, `Test` is the instance not the constructor
转换构造函数

现在我们需要表示一个构建这些TransformedTest实例的构造函数。我们可以手动编写一个带有与构造函数匹配的参数的构造函数Test

type TransformedTestCtor = new(/*constructor arguments*/) => TransformedTest;

或者我们可以编写一个类型,它接受一个构造函数并返回一个接受相同 arg 但构造不同实例的构造函数:

type ClassTransform<
  OldClass extends new (...args: any[]) => any,
  NewType
> = OldClass extends new (...args: infer Args) => any
  ? new (...args: Args) => NewType
  : never;

// Test - as a value is the constructor, so `typeof Test` is the type of the constructor
type TransformedTestCtor = ClassTransform<typeof Test, TransformedTest>;
用法

所以现在我们有一个构造函数,它接受相同的参数但返回一个不同的实例。我们如何实际使用它?

不幸的是,这样的语法不起作用:

whatever(Test)

您通常可以使用asserts签名更改函数参数的类型,但它不适用于类。

所以,没有比仅仅断言类型更好的方法了:

const TransformedTest = Test as unknown as TransformedTestConstructor;

通过将此构造函数命名为与我们之前定义的实例类型相同,我们模仿了构造函数(值)和实例(类型)共享相同名称的常见模式。(并且可以,例如一起导出)

另一种选择是将其放入返回转换后类型的函数中:

function transformType(Type: typeof Test) {
    return Test as unknown as TransformedTestConstructor;
}
const TransformedTest = transformType(Test);

这将Test作为转换后的构造函数返回:但它不会TransformedTest像普通类一样将类型带入范围 - 它只会将构造函数(作为值)带入范围。所以如果Test是可变的并且你这样做:

Test = transformType(Test);

然后Test 将是新的构造函数,但类型仍将是旧实例。


这是 Typescript Playground 中此答案的代码


推荐阅读