Welcome
In our project, we have implemented attributes support where each attribute acts as a class. These attributes include information on type, optionality, and name. Instead of creating an interface for every entity, my goal is to automate this process. With approximately 500 attributes and 100+ entities, each entity serves as a collector for various attributes.
Illustration
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 observe that I utilize valueType
to determine the actual type. Additionally, I am aware of the name and whether it is optional or required based on the required
attribute.
Idea and Current Approach
My approach involves iterating over the attributes
array, mapping values to names, and specifying optionality.
- Type to filter optional attributes
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 attributes
type FilterRequiredAttribute<Attr extends AttributeClass> = FilterOptionalAttribute<Attr> extends never ? Attr : never
- Type to convert types to primitive types
type ExtractPrimitiveType<A> =
A extends StringConstructor ? string :
A extends NumberConstructor ? number :
A extends BooleanConstructor ? boolean :
never
- Type to convert classes to key-value objects (with required + optional attributes)
type AttributeDataType<Attr extends AttributeClass> = { [K in Attr["attrName"]]: ExtractPrimitiveType<Attr["valueType"]> }
type OptionalAttributeDataType<Attr extends AttributeClass> = { [K in Attr["attrName"]]?: ExtractPrimitiveType<Attr["valueType"]> }
- Combining the above and inferring array types
type UnboxAttributes<AttrList> = AttrList extends Array<infer U> ? U : AttrList;
type DataType<AttributeList extends AttributeClass[]> = OptionalAttributeDataType<FilterOptionalAttribute<UnboxAttributes<AttributeList>>> & AttributeDataType<FilterRequiredAttribute<UnboxAttributes<AttributeList>>>
Expected Outcome
class SomeClass {
static attributes = [AttrTest, Attr2Test]
}
// Expected output with double equals
const mapped: DataType<typeof SomeClass.attributes> == {
test: string
test2?: number
}
Current Behavior
Upon checking through IntelliJ IDEA Ultimate:
// The IDE displays mixed types even when inference is used
const mapped: DataType<typeof SomeClass.attributes> == {
test: string | number
test2: number | number
}
I have spent 5 hours attempting to resolve these issues. It seems like there's a crucial element missing from my solution. I appreciate any guidance or tips provided by those who understand where I may be going wrong.
There are two main concerns:
- All attributes are shown as required (while 'test2' should actually be optional)
- The types are jumbled up despite my efforts to infer them correctly
For further exploration, please refer to the TypeScript Playground