英文:
Creating a sub-type and sub-class from a struct definition
问题
我正在尝试编写一个能够接受结构定义并生成类型和类定义以像普通类一样使用的类。
我认为最佳的结果可能是这样的(尽管我很灵活,因为我的主要目标是在TS中解析和使用结构体):
interface TestFields {
hello: 'a',
}
interface BooFields {
world: Struct<TestFields>,
wow: 'b'
}
export const TestStruct = Struct.define<TestFields>({
hello: 'a'
});
export const BooStruct = Struct.define<BooFields>({
world: TestStruct,
wow: 'b'
});
let myVar = new BooStruct();
myVar.wow = 1;
myVar.world.hello = 4;
我正在尝试的代码的精简版本:
export type FieldTypes = 'a' | 'b';
export type NestedFieldTypes = FieldTypes | Struct<any>;
export interface FieldDefinition {
type: NestedFieldTypes;
name: string;
offset: number;
}
export type StructFields<T> = {
[K in keyof T]: T[K] extends Struct<infer U>
? U
: FieldTypes;
};
export class Struct<T extends StructFields<T>> {
public static readonly TypeSizes: { [K in FieldTypes]: number } = {
a: 1,
b: 2
};
public static readonly FieldDefinitions: FieldDefinition[] = [];
public get length(): number {
return 1;
}
constructor() {}
public static define<T extends StructFields<T>>(fields: T): typeof Struct<T> {
const fieldDefinitions: FieldDefinition[] = [];
let currentOffset = 0;
for (const name in fields) {
const fieldType = fields[name];
currentOffset += Struct.TypeSizes[fieldType as FieldTypes];
fieldDefinitions.push({ type: fieldType, name: name, offset: currentOffset });
}
let result = Struct<T>;
result.FieldDefinitions.push(...fieldDefinitions);
return result;
}
}
我尝试过在各种地方使用或不使用 typeof Struct<X>
。
我还删除了 Struct<X>[]
实现,因为它增加了复杂性(因此有注释的代码)。
我的问题要么是 define
函数的结果是对象而不是类,要么是我无法将 fieldDefinitions
中的 type
定义为 fieldType
。
编辑:我想提供更多背景信息。当我说“结构体”时,我指的是C结构体。这个脚本是为了Frida,所以 wow
可以是 uint32
类型,并且仅接受 number
值。
英文:
I am trying to write a class that is able to take a struct definition and generate a type and class definition to be used like a regular class.
I think that an optimal result would be something like this (although I'm flexible as my greater target is to parse and use a struct in TS):
interface TestFields {
hello: 'a',
}
interface BooFields {
world: Struct<TestFields>,
wow: 'b'
}
export const TestStruct = Struct.define<TestFields>({
hello: 'a'
});
export const BooStruct = Struct.define<BooFields>({
world: TestStruct,
wow: 'b'
});
let myVar = new BooStruct();
myVar.wow = 1;
myVar.world.hello = 4;
A very slimmed down version of the code that I'm trying:
export type FieldTypes = 'a' | 'b';
export type NestedFieldTypes = FieldTypes | Struct<any>;
export interface FieldDefinition {
type: NestedFieldTypes;
name: string;
offset: number;
}
export type StructFields<T> = {
[K in keyof T]: T[K] extends Struct<infer U>
? U
: FieldTypes;
};
export class Struct<T extends StructFields<T>> {
public static readonly TypeSizes: { [K in FieldTypes]: number } = {
a: 1,
b: 2
};
public static readonly FieldDefinitions: FieldDefinition[] = [];
public get length(): number {
return 1;
}
constructor() {}
public static define<T extends StructFields<T>>(fields: T): typeof Struct<T> {
const fieldDefinitions: FieldDefinition[] = [];
let currentOffset = 0;
for (const name in fields) {
const fieldType = fields[name];
// if (fieldType instanceof Struct) {
// currentOffset += (fieldType as Struct<any>).length;
// fieldDefinitions.push({ type: new (fieldType as Struct<T>)(), name: name, offset: currentOffset });
// } else if (Array.isArray(fieldType) && fieldType.every(value => value instanceof Struct)) {
// for (const nestedStruct of fieldType) {
// currentOffset += nestedStruct.length;
// }
// } else {
currentOffset += Struct.TypeSizes[fieldType as FieldTypes];
fieldDefinitions.push({ type: fieldType, name: name, offset: currentOffset });
// }
}
let result = Struct<T>;
result.FieldDefinitions.push(...fieldDefinitions);
return result;
}
}
I tried playing around with/without typeof Struct<X>
in various places
Also I removed the Struct<X>[]
implementation cause it added complexity (hence the commented out code)
My problems are either that the result of the define
function is an object and not a class or that I can't define the type
in fieldDefinitions
to fieldType
EDIT: I'd like to give a bit more context. When I say "struct", I mean a C struct. The script is intended for frida so wow
could be of type "uint32"
and accept only values of number
答案1
得分: 1
Sure, here's the translated code portion:
我将创建一个名为 `Structify()` 的 *类工厂函数*,它接受一个描述所需类形状的模式对象,并生成相应的新类构造函数。之所以需要一个模式对象而不仅仅是一个类型,是因为在运行时,[TypeScript 类型已经被 *擦除*](https://www.typescriptlang.org/docs/handbook/2/basic-types.html#erased-types)。所以我们需要一个实际的值。幸运的是,类型可以从该值中推断出来,因此我们不需要重复自己。
所需的行为将如下所示:
```typescript
const TestStruct = Structify({
hello: 'uint32'
});
const BooStruct = Structify({
world: TestStruct,
wow: 'int64'
})
let myVar = new BooStruct();
/* let myVar: {
world: {
hello?: number | undefined;
};
wow?: number | undefined;
} */
myVar.wow = 1;
myVar.world.hello = 4;
console.log(myVar instanceof BooStruct) // true
console.log(myVar.world instanceof TestStruct) // true
正如您所看到的,模式对象的属性要么是类似 "uint32"
的字符串,对应于某些已知的字段类型,要么是类似 TestStruct
的零参数类构造函数。还请注意,对于这些已知字段类型,生成的类字段都是可选的;这是因为字段没有默认值(至少在我的实现中没有),因此如果只写 new TestStruct()
,hello
属性将开始为 undefined。所以我们不是 {hello: number}
,而是 {hello?: number}
。
好的,让我们来实现它。首先,我们需要一个描述已知字段的类型,就像这样:
interface FieldMap {
uint32: number;
int64: number;
string: string;
}
您可以根据需要添加或更改字段。请注意,我不打算对类型进行任何运行时验证,以查看例如 "uint32"
是否为非负数且位于适当的范围内。如果您需要这样做,您将不得不修改实现。
好的,这是代码:
function Structify<
T extends Record<keyof T, (new () => any) | keyof FieldMap>
>(schema: T) {
type O = (
{ [K in keyof T]?: unknown } &
{ [K in keyof T as T[K] extends keyof FieldMap ? K : never]?:
FieldMap[T[K]] } &
{ [K in keyof T as T[K] extends keyof FieldMap ? never : K]:
T[K] extends new () => infer O ? O : never }
) extends infer O ? { [K in keyof O]: O[K] } : never;
return class {
constructor() {
Object.entries(schema).forEach(([k, v]: [string, any]) => {
if (typeof v === "function") (this as any)[k] = new v();
});
}
} as new () => O;
}
这个函数是 泛型的,类型参数 T
对应于输入的 schema
。它被 限制,以便每个属性都是零参数构造函数签名或 FieldMap
的键。
在函数内部,我们计算类型 O
,它对应于输出类的实例类型。这有点混乱,但要点是我们映射 T
的属性,对于每个属于 FieldMap
键的属性,我们创建一个可选属性,其类型是 FieldMap
的相应值;而对于每个构造函数属性,我们创建一个类型为该构造函数的实例类型的必需属性。
特定的实现使用了键重映和条件类型推断来构建 O
,以便输出尽可能“漂亮”。它类似于这样的东西:
type O = { [K in keyof T]:
T[K] extends keyof FieldMap ?
FieldMap[T[K]] | undefined :
T[K] extends new () => infer O ? O : never
}
这可能更容易理解,尽管在这种情况下 FieldMap
属性不是可选的(尽管它们允许 undefined
)。
无论如何,函数实现返回一个不带参数的 类表达式。该构造函数通过在 config
中调用任何构造函数并将结果对象分配给类实例的相应属性(即 this
)来初始化类实例。请注意,有多个类型断言和对any 类型的使用以放宽类型检查。这是因为编译器不可能理解逻辑来验证实现的返回值是否与复杂的泛型条件返回类型匹配。因此,我们必须小心确保实现是正确的。
好的,让我们来测试一下。上面关于 TestStruct
和 BooStruct
的内容按预期工作。让我尝试一个不同的示例:
const Person = Structify({
name: "string",
age: "int64",
})
const PairOfPeople = Structify({
person1: Person
<details>
<summary>英文:</summary>
I will sketch an implementation of a *class [factory function](https://en.wikipedia.org/wiki/Factory_(object-oriented_programming))* called `Structify()` that takes a schema object describing the desired shape of the class, and produces a new class constructor corresponding to it. The reason why we need a schema object and not just a type is because, at runtime, [TypeScript types have already been *erased*](https://www.typescriptlang.org/docs/handbook/2/basic-types.html#erased-types). So we need an actual value. Luckily the type can be inferred from that value, so we won't need to repeat ourselves.
The desired behavior will look something like this:
const TestStruct = Structify({
hello: 'uint32'
});
const BooStruct = Structify({
world: TestStruct,
wow: 'int64'
})
let myVar = new BooStruct();
/* let myVar: {
world: {
hello?: number | undefined;
};
wow?: number | undefined;
} */
myVar.wow = 1;
myVar.world.hello = 4;
console.log(myVar instanceof BooStruct) // true
console.log(myVar.world instanceof TestStruct) // true
As you can see, the schema object's properties are either a string like `"uint32"` corresponding to some known field type, or a zero-arg class constructor like `TestStruct`. Also note that for those known field types, the resulting class fields are [optional](https://www.typescriptlang.org/docs/handbook/2/objects.html#optional-properties); that's because there's no *default* value for the fields (at least in my implementation), so if you just write `new TestStruct()`, the `hello` property will start off undefined. So instead of `{hello: number}` we have `{hello?: number}`.
---
Okay, let's implement it. First we need a type describing the known fields, like this:
interface FieldMap {
uint32: number;
int64: number;
string: string;
}
You can add or change the fields as desired. Note that I am not planning to have any runtime validation of the types to see that, for example, `"uint32"` is non-negative and in the appropriate range. If you want that you'll have to modify the implementation.
Okay, here goes:
function Structify<
T extends Record<keyof T, (new () => any) | keyof FieldMap>
>(schema: T) {
type O = (
{ [K in keyof T]?: unknown } &
{ [K in keyof T as T[K] extends keyof FieldMap ? K : never]?:
FieldMap[T[K]] } &
{ [K in keyof T as T[K] extends keyof FieldMap ? never : K]:
T[K] extends new () => infer O ? O : never }
) extends infer O ? { [K in keyof O]: O[K] } : never;
return class {
constructor() {
Object.entries(schema).forEach(([k, v]: [string, any]) => {
if (typeof v === "function") (this as any)[k] = new v();
}
)
}
} as new () => O;
}
This function is [generic](https://www.typescriptlang.org/docs/handbook/2/generics.html) in the type parameter `T` corresponding to the `schema` input. It is [constrained](https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints) so that each property is either a zero-arg [construct signature](https://www.typescriptlang.org/docs/handbook/2/functions.html#construct-signatures) or a key of `FieldMap`.
Inside the function we compute the type `O` corresponding to the instance type of the output class. It's kind of messy, but the point is that we [map](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html) over the properties of `T`, and for each property that's a key of `FieldMap` we make an optional property whose type is the corresponding value of `FieldMap`; while for each property that's a constructor, we make a required property whose type is the instance type of that constructor.
The particular implementation uses [key remapping](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as) and [conditional type inference](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#inferring-within-conditional-types) to construct `O` in such a way that the output is as "pretty" as I can make it. It's similar to something like
type O = { [K in keyof T]:
T[K] extends keyof FieldMap ?
FieldMap[T[K]] | undefined :
T[K] extends new () => infer O ? O : never
}
which might be easier to comprehend, although in this case the `FieldMap` properties are not optional (although they do allow `undefined`).
Anyway, the function implementation returns a [class expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/class) whose constructor takes no arguments. That constructor initializes the class instance by invoking any constructors in from `config` and assigning the resulting object to the corresponding property of the class instance (which is `this`). Note that there are multiple [type assertions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions) and uses of [the `any` type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#any) to loosen the type checking. That's because the compiler cannot possibly understand the logic to verify that the implementation's retyrn value matches the complicated generic conditional return type. So we have to be careful that the implementation is correct.
----
Okay, let's test it out. The stuff with `TestStruct` and `BooStruct` above works as expected. Let me try a different example:
const Person = Structify({
name: "string",
age: "int64",
})
const PairOfPeople = Structify({
person1: Person,
person2: Person
})
So a `Person` should have (optional) `name` and `age` properties, and a `PairOfPeople` has two `Person` properties. Does it?
const people = new PairOfPeople();
/* const people: {
person1: {
name?: string | undefined;
age?: number | undefined;
};
person2: {
name?: string | undefined;
age?: number | undefined;
};
} */
people.person1.name = "John";
people.person1.age = 35;
people.person2.name = "Jane";
people.person2.age = 30;
console.log(people)
/* {
"person1": {
"name": "John",
"age": 35
},
"person2": {
"name": "Jane",
"age": 30
}
} */
Yes, looks good!
[Playground link to code](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgGLAgGwCYFk4AOyA3gLABQyVyArqGAMwBMAXMiDQLYBG0A3BWrJ6ANgAsbDj36DqAZzBRQAczYKlIZQPIBfCvvIwaIBGGAB7EMgDKimqeAwAngB5ZVACrIIAD0ghsOWQAJQgEcyhsFwBrCCdzGGQPABpkAAoQCAB3dIBKZABeAD5kOBAnfIAfZFj4xPQsPEIiiiK0uQQACwhOODYPfLJyd2QwJwIUAHlC9JGhYmQAbQBpYStahKSAXQB+NmNokHMsqx1kADI56gWVtZq4za84II8Vre8-CACgjfqMHHwRB2yFWkggADdoLsWFchFQGgDCItXsstu8zpdKHCqDdVqB7nUkqUXm8Pv5AgTNgimkD2BDoMg2KiYVjsUIUe9fOSgpkcml8sU1jAGdNgdMwZCoMg9KzkPkuV8KaBhVLRSQlnj1g9EpMtmxJqSzhKZMNTUIoBAwDQoFYEJhnkEhmzkOEQOp7GAIvySLC4ZNuAArMJgAB0X0UGDk7S6PTguRDMAiAFFEJ00mlFtFUuC9Ut1CpUmUnFsBSUnc62Y50mMJptwYUCgVkAAiIwmMyWZv5NJgTrAILPUrlXKZ95N3nIcH87QV6gy2dUXK++dz4l0vml5CTbQygwUV0KJIQBS2KAemanj2OJxpctUbqYTDmNgAcjo4GYL4oOly2n3lkPAAhcxzEvUwLzsBxnFvEYsgiHB+mPMAwLAZJYOOV9RDEL9dCXU1MEtZBOCcAA1OApXHbJkGA0DILAacKAAegAKmQAiwCI0jyLYO9kDgqAEJ9WUhAfJ89nYLheClapjGwCAYFACBsBnOcVKoOCsnEqQpOQGSAnkxTlO-ZBmMYgxiLIqAQw0mYAEZtAs8jrPg7AQ1E8wZjEP9yAPcwCJDJ9lDSRypVABQyiQTYaJQ-JGMY0Yzwgf83T8iAAvMIKQucgTsDWcKTAgR4kJi5A4oSmgktNZLDwABWgORLAgs8oJvXiQDgTgIDYZt800Zs0NlOBlC6lssP6788OqjiargYAoEmGA6vMAgCKaq9oN4iYoAakBbLYOrtssAahC2nbWGQA6domgwDw42sVpQSichmuaFqWh6GPIFiXQAu7CoeniRlOyw9qE512s68TeuUXTaH0hTMiM4TqCGiAtMkhk9LkhGlLU6U8eBkBzt4oQIbRtQI00WHZIMxG8aEVH0ekaS4exwy8Z0HcTLM8h7v8wnbJDMmZmbAApcxOhAZttD5tKBZDVGZgYABWGX-v5+rLCYIWOseltRbKCBpYoWWQ0J7XFabBgAAZtF8-zArSWW8O+8tmwF5tAdlZsyc9-WJal47qGbVG-ZV2QdCD93NaJv3eJ93W-bFw3xu90O2BtiPjNMiggA)
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论