【问题标题】:TypeScript: Cast whole class typeTypeScript:强制转换整个类类型
【发布时间】:2020-05-26 05:04:24
【问题描述】:

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

假设我有以下课程:

class Test {
  public myNumber = 7;
}

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

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

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

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

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

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

whatever(Test)

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

所以现在应该可以了:

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

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

===

因为在本期的 cmets 中提到了上下文(如何使用该类),所以我想另外提供以下示例。它是一个使用 jasminekarmaAngular 组件的测试(规范)文件。我试图用代码 sn-p 中的 cmets 来解释自己。

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();
  });
});

===

提前致谢, 卢卡斯

【问题讨论】:

  • 这在某种程度上取决于您计划如何使用该课程。就像您可以在导出时应用类型转换一样,但在导出模块中语言将不可用。
  • 我在问题描述的末尾添加了更多信息。希望这会有所帮助。目前我不太确定你的建议是什么。

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


【解决方案1】:

这是一种奇怪的模式,我不建议轻率地做这件事,但它在很大程度上是可以实现的:

实例和构造函数

当你声明你的类 Test 时,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;

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

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);

那么 value Test 将是新的构造函数,但 type 仍将是旧实例。


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

【讨论】:

  • 感谢您的详细解答。对此,我真的非常感激。我肯定会更深入地了解您的建议。您能否根据您的建议添加我的示例(测试文件的代码 sn-p / 描述块 || 位于我的问题描述末尾)?这将帮助我更好地理解它。提前致谢。
  • 只写Test = transformType(Test); 行不行,对吧?我理解正确吗?这就是为什么我真的很期待看到我的例子和你的建议。
【解决方案2】:

看起来你正在变成间谍实例。对吗?

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

例子:

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 中的类型系统。起初,如果来自其他语言,可能会有点难以理解。

我们需要首先认识到类型在编译期间只存在。它们在那里是为了告诉您的 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-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

注意事项

您的方法很可能会简单得多。它也可能会产生一些不好的后果,所以我添加了一些可能会有所帮助的注释。

  • 看起来您的类是在测试环境之外定义和使用的
  • 您似乎还想在测试期间自动将所有功能变成间谍。

如果是这种情况,使用转换器将所有功能普遍变成间谍是不好的做法。您不会希望将 jasmine 代码实现到在测试环境之外运行的东西中。

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

Transformer 比您需要的要复杂得多,而且肯定不是一个好主意,仅用于测试期间需要的东西。

我看到你有TestBed.createComponent。假设 createComponent 存在于测试空间中,这可能就是我要输入的逻辑:

  1. 迭代 Object.getOwnPropertyDescriptors() -> 如果是方法或函数,则使用该方法/函数的 Jasmine spy 更新对象
  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
}

你想要最少的工作量——当我告诉你的时候相信我,一个变压器几乎总是不是正确的路线。 ?

【讨论】:

  • 是的,我正在更改为间谍实例。 class Test { public greet(): string { return "Hello"; } } ...将是例如改为... class Test { public greet = jasmine.createSpy(); }
  • 对于您在 SpyifyFunctions 中建议的行 [K in keyof T]: T[K] extends function ? jasmine.Spy : T[K],我很遗憾收到错误消息:找不到名称函数。有什么想法吗?
  • 无论如何我喜欢你的建议的简单性。但在最好的情况下,我想摆脱使用映射类型。如果可能的话,我不想触及 componentfixture 的类型定义。它们应该都是ParentComponent 类型。我还希望不必在 beforeEach 中进行演员表。在最好的情况下,我只想写ttransformer(ParentComponent),然后从现在开始改变类型。我正在考虑类似于 type guard 的东西。我知道这可能很不寻常,但我想尽可能少写代码。
  • 但是之前评论中解释的愿景大概是不可能的吧?我还不完全理解这个答案(stackoverflow.com/a/61991234/13513418)。但如果写ParentComponent = ttransformer(ParentComponent) 是唯一必须做的事情,我会非常兴奋。这将是我瞄准的方向。但也许那是不可能的。这就是我问的原因。我脑子里有类似 type guard 之类的东西,这说明我的想法可能是最好的。
  • 对不起,应该是 Function(大写 F)。我已经纠正了。至于其余的,我会在回复中详细回复并编辑
猜你喜欢
  • 2020-11-16
  • 2021-01-24
  • 2020-08-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多