Introduction
At our project we have attributes support where each attribute is class. The attribute contains info about type, optionality and name. Instead of defining an interface for each entity, I would like to automatize it. We have around 500 attributes and 100+- entities. An entity is a collector for attributes.
Example
interface AttributeClass {
readonly attrName: string;
readonly required?: boolean;
readonly valueType: StringConstructor | NumberConstructor | BooleanConstructor;
}
class AttrTest extends Attribute {
static readonly attrName = "test";
static readonly required = true;
static readonly valueType = String
}
class Attr2Test extends Attribute {
static readonly attrName = "test2";
static readonly valueType = Number
}
interface Entity {
test: string // AttrTest
test2?: number // Attr2Test
}
class SomeClass {
static attributes = [AttrTest, Attr2Test]
}
Here you can notice that, I have valueType which holds the real type. I also know the name and if is it optional. (required if the required exists and is set to true)
Concept and my not working solution
My idea is to iterate over the attributes array, map the value to the name and make it optional.
- Type to filter optional attribute
export type ValueOf<T> = T[keyof T];
type FilterOptionalAttribute<Attr extends AttributeClass> = ValueOf<Attr["required"]> extends false | undefined | null ? Attr : never
- Type to filter required attribute
type FilterRequiredAttribute<Attr extends AttributeClass> = FilterOptionalAttribute<Attr> extends never ? Attr : never
- Type to convert from Type to primitive type
type ExtractPrimitiveType<A> =
A extends StringConstructor ? string :
A extends NumberConstructor ? number :
A extends BooleanConstructor ? boolean :
never
- Type to convert from class to key-value object (required + optional)
type AttributeDataType<Attr extends AttributeClass> = { [K in Attr["attrName"]]: ExtractPrimitiveType<Attr["valueType"]> }
type OptionalAttributeDataType<Attr extends AttributeClass> = { [K in Attr["attrName"]]?: ExtractPrimitiveType<Attr["valueType"]> }
- Glue it together + something to infer out the array type
type UnboxAttributes<AttrList> = AttrList extends Array<infer U> ? U : AttrList;
type DataType<AttributeList extends AttributeClass[]> = OptionalAttributeDataType<FilterOptionalAttribute<UnboxAttributes<AttributeList>>> & AttributeDataType<FilterRequiredAttribute<UnboxAttributes<AttributeList>>>
What do I expect on output
class SomeClass {
static attributes = [AttrTest, Attr2Test]
}
// notice double equals
const mapped: DataType<typeof SomeClass.attributes> == {
test: string
test2?: number
}
What IDE shows
using IntelliJ IDEA Ultimate
// notice double equals
const mapped: DataType<typeof SomeClass.attributes> == {
test: string | number
test2: number | number
}
I spend already 5 hours to solve it. Seems I'm missing something important. I would like to thanks everyone who gives me any tip what I'm doing wrong.
There are two issues:
- Everything is required (test2 should be optional)
- Types are mixed even when I'm inferring them
Link for TypeScript Playground
Attribute. Can you run your code through something like The Playground and edit until you get a minimal reproducible example that demonstrates the issue you have and only that issue?staticproperties and just give the interfaces you're trying to transform. I will likely do something similar when I write up my answer.