我想知道是否可以更改类属性的类型?但不是简单的转换,而是覆盖整个类的类型。
假设我有以下课程:
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();
});
});
===
预先感谢, 卢卡斯
答案 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(在右侧面板上)
单击上面的链接,以查看如何剥离此代码的所有类型信息。
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
应该也有助于理解的一个关键点是,一旦分配了引用,就无法更新分配给引用的类型。
请记住,在运行转换器之前,先在 运行类型并分配它们。信不信由你,这很好。否则会很混乱。
请牢记:
这是使用几种帮助程序类型的一种方法。鉴于您的用例,这可能是执行您要尝试执行的操作的最佳途径。
/* ************************************************************************************* */
// 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
您的方法很有可能会更简单。它还可能会带来一些不良后果,因此,我添加了一些说明可能会有所帮助。
如果是这种情况,那么使用转换器将所有功能通用化为间谍将是一种不好的做法。您不想将茉莉花代码实现到在测试环境之外运行的某些程序中。
相反,更好(更简单)的方法是编写一个枚举类实例的属性描述符的函数,并使其返回类型使用映射类型,以便您的测试了解发生了什么。
变压器要比您需要的复杂得多,并且将其用于只在测试期间需要的东西肯定不是一个好主意。
我看到您有TestBed.createComponent
。假设createComponent
仅存在于测试空间中,那可能就是我在逻辑中放置的地方:
类似
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
}
您只需要最少的工作-当我告诉您时,请相信我,这台变压器几乎总是不走正确的路。 ?