TypeScript:强制转换整个类类型

时间:2020-05-24 17:44:53

标签: typescript typescript-typings typescript-compiler-api

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

假设我有以下课程:

class Test {
  public myNumber = 7;
}

是否可以将 myNumber 属性的类型从 number 更改为例如字符串

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

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

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

我首先想到的是,可以使用索引类型来实现。但是我想问问是否有人已经对此有经验或有一个具体的想法。

例如调用函数会很酷...

whatever(Test)

...,然后更改类的类型。所以编译器应该从现在开始知道 myNumber 应该是 string 类型,而不是 number 类型。

所以现在应该有可能:

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

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

===

因为在此问题的评论中提到了上下文(如何使用该类),所以我想另外提供以下示例。它是使用茉莉花 karma 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();
  });
});

===

预先感谢, 卢卡斯

2 个答案:

答案 0 :(得分:3)

这是一种奇怪的模式,我不建议您轻描淡写,但这在很大程度上是可以实现的:

实例和构造函数

当您声明类Test时,Typescript确实可以做两件事:名为Test的类型,它是Test类的实例,以及一个值也名为{{1} },它是构建类型Test的构造函数。 TS可以给它们起相同的名称,并使用单个Test声明将它们带入范围,但是我们必须分别处理它们。

转换实例

因此,处理实例时,我们可以编写一种类型,将带有数字的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

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

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

因此,现在我们有了一个构造函数,该构造函数采用相同的参数,但返回不同的实例。我们该如何实际使用呢?

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

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

然后 value 测试将是新的构造函数,但 type 仍将是旧实例。


Here's the code from this answer in the Typescript Playground

答案 1 :(得分:0)

好像您要更改为间谍实例。那是对的吗?

无论如何,我认为这里的简单解决方案是使用映射类型来更改组件的类型。

示例:

type SpyifyFunctions<T> = {
  // If jasmine.Spy can be used with type arguments, you could supply that here, too
  [K in keyof T]: T[K] extends Function ? jasmine.Spy : T[K]
}

describe('ParentComponent', () => {
  let component: SpyifyFunctions<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; // If necessary, cast here (as SpyifyFunctions<ParentComponent>)
    fixture.detectChanges();
  });

  it('test it', () => {
    component.ttransformerTest.and.returnValue('Transformer Test');
    expect(component.ttransformerTest).toHaveBeenCalled();
  });
});

编辑

此问题需要了解TypeScript中的Type系统。起初,从其他语言来理解可能有点困难。

我们需要首先认识到类型在编译过程中仅存在。它们在那里是为了告诉您的IDE或编译器应该是什么。这是通过提供错误并为您的IDE提供智能帮助来帮助您。当代码实际运行时,类型根本不存在!了解这一点的一个好方法是去打字机游戏场看一下输出的javascript(在右侧面板上)

看看this example

单击上面的链接,以查看如何剥离此代码的所有类型信息。

type Spy<TFunction> = { fn: TFunction }

class ParentComponent {
    // We assume an external transformer will change ttransformerTest from a method to a property with the shape { fn: (original method function) }
    ttransformerTest(): string { return '' }
    p!: string
}

type SpyifyFunctions<T> = {
  [K in keyof T]: T[K] extends Function ? Spy<T[K]> : T[K]
}

// Here we're specifically telling TypeScript what Type the component is, so that we don't get errors trying to use component.fn
const component = new ParentComponent() as unknown as SpyifyFunctions<ParentComponent>;

component.ttransformerTest.fn() // <-- No errors, because TypeScript recognizes this as the proper type for component

应该也有助于理解的一个关键点是,一旦分配了引用,就无法更新分配给引用的类型

请记住,在运行转换器之前,先在 运行类型并分配它们。信不信由你,这很好。否则会很混乱。

请牢记:

  • 我们现在知道,转换器不会影响我们的IDE或编译器中的错误,因为它们是在代码经过分析之后发生的。
  • 由于JavaScript方面已由转换器负责,因此我们现在需要找到一种以IDE可以识别的方式“转换”实际Type的方法。

这是使用几种帮助程序类型的一种方法。鉴于您的用例,这可能是执行您要尝试执行的操作的最佳途径。

/* ************************************************************************************* */
// type-transformers.ts
/* ************************************************************************************* */

type Spy<TFunction> = { fn: TFunction }

type SpyifyFunctions<T> = {
  [K in keyof T]: T[K] extends Function ? Spy<T[K]> : T[K]
}

export type MakeSpyableClass<T extends new (...args: any[]) => any> = 
  T extends new (...args: infer Args) => any ? 
    new (...args: Args) => SpyifyFunctions<InstanceType<T>> : never;


/* ************************************************************************************* */
// ParentComponent.ts
/* ************************************************************************************* */
import { MakeSpyableClass } from './type-transformers.ts'

// Note that we do not export this class, directly, since we need TS to infer its type before 
// we "transform" it as an export
class ParentComponentClass {
    // We assume an external transformer will change ttransformerTest from a method to a 
    // property with the shape { fn: (original method function) }
    ttransformerTest(): string { return '' }
    p!: string
}

// Here is where we create a new exported reference to the class with a specific type that we 
// assign We cast to unknown first, so TypeScript doesn't complain about the function shapes 
// not matching (remember, it doesn't know about your tranformer)
export const ParentComponent = 
  ParentComponentClass as unknown as MakeSpyableClass<typeof ParentComponentClass>


/* ************************************************************************************* */
// example.ts
/* ************************************************************************************* */
import { ParentComponent } from './ParentComponent.ts'

const component = new ParentComponent();
component.ttransformerTest.fn() // <-- Recognizes type as Spy instance

See it in Playground

注意事项

您的方法很有可能会更简单。它还可能会带来一些不良后果,因此,我添加了一些说明可能会有所帮助。

  • 似乎您的类已定义并在测试环境之外使用
  • 您似乎还想在测试过程中将所有功能自动变成间谍。

如果是这种情况,那么使用转换器将所有功能通用化为间谍将是一种不好的做法。您不想将茉莉花代码实现到在测试环境之外运行的某些程序中。

相反,更好(更简单)的方法是编写一个枚举类实例的属性描述符的函数,并使其返回类型使用映射类型,以便您的测试了解发生了什么。

变压器要比您需要的复杂得多,并且将其用于只在测试期间需要的东西肯定不是一个好主意。

我看到您有TestBed.createComponent。假设createComponent仅存在于测试空间中,那可能就是我在逻辑中放置的地方:

  1. 迭代Object.getOwnPropertyDescriptors()->如果是方法或函数,请使用该方法/函数的Jasmine间谍更新对象
  2. 函数的返回类型应使用映射类型帮助器

类似

type SpyifyFunctions<T> = {
  [K in keyof T]: T[K] extends Function ? Spy<T[K]> : T[K]
}

createComponent<T extends new (...args: any[]) => any>(component: T): SpyifyFunctions<InstanceType<T>> {
// Create new component
// Iterate descriptors and replace with spies
// return component
}

您只需要最少的工作-当我告诉您时,请相信我,这台变压器几乎总是不走正确的路。 ?