diff --git a/package.json b/package.json index 8fb25d8..e7b1a5b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@thilawyn/thilatrait", - "version": "0.1.1", + "version": "0.1.2", "type": "module", "publishConfig": { "registry": "https://git.jvalver.de/api/packages/thilawyn/npm/" diff --git a/src/expresses.ts b/src/expresses.ts new file mode 100644 index 0000000..5784e56 --- /dev/null +++ b/src/expresses.ts @@ -0,0 +1,84 @@ +import { AbstractClass, Opaque } from "type-fest" +import { Trait, TraitApplierSuperTag } from "." +import { ClassesInstances, ClassesStaticMembers, MergeInheritanceTree, MergeInheritanceTreeWithoutOverriding, StaticMembers, TraitsClasses } from "./util" + + +/** + * Extends a class with the given traits and expresses their combined functionality. + * @template C - The abstract class type. + * @template Traits - An array of traits. + * @param extend - The class to extend. + * @param traits - An array of traits to apply. + * @returns A new class type expressing the combined functionality of the base class and traits. + * @example + * Extends a superclass and applies traits: + * ```ts + * class User extends extendsAndExpresses(Entity, + * Identifiable(), + * Permissible, + * ) { + * readonly id: bigint + * + * constructor(id: bigint) { + * super() + * this.id = id + * } + * } + * ``` + */ +export function extendsAndExpresses< + C extends AbstractClass, + Traits extends readonly Trait[], +>( + extend: C, + ...traits: Traits +) { + return traits.reduce( + (previous, trait) => trait(previous), + extend as Opaque, + ) as unknown as ( + AbstractClass< + MergeInheritanceTreeWithoutOverriding<[ + InstanceType, + ...ClassesInstances< + TraitsClasses + >, + ]>, + + ConstructorParameters + > & + + MergeInheritanceTree<[ + StaticMembers, + ...ClassesStaticMembers< + TraitsClasses + >, + ]> + ) +} + +/** + * Expresses the combined functionality of multiple traits. + * @template Traits - An array of trait. + * @param traits - An array of trait to apply. + * @returns A new class type expressing the combined functionality of the traits. + * @example + * Applies traits to a class: + * ```ts + * class User extends expresses(Identifiable(), Permissible) { + * readonly id: bigint + * + * constructor(id: bigint) { + * super() + * this.id = id + * } + * } + * ``` + */ +export function expresses< + Traits extends readonly Trait[], +>( + ...traits: Traits +) { + return extendsAndExpresses(Object, ...traits) +} diff --git a/src/index.ts b/src/index.ts index 0d273bc..ae86066 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,205 +1,2 @@ -import { AbstractClass, AbstractConstructor, Opaque, UnionToIntersection } from "type-fest" - - -/** - * Represents the static members of a class. - * @template C - The class type. - */ -type StaticMembers = { - [Key in keyof C as Key extends "prototype" ? never : Key]: C[Key] -} - - -/** - * Represents a trait that can be applied to a class. - * @template C - The abstract class type. - */ -export type Trait< - C extends AbstractClass -> = Opaque< - TraitApplier, - "thilatrait/Trait" -> - -/** - * Represents the function signature for applying a trait to a parent class. - * @template C - The abstract class type. - */ -export type TraitApplier< - C extends AbstractClass -> = - (Parent: AbstractConstructor) => C - -/** - * Returns the class type of a trait. - * @template T - The trait type. - */ -export type TraitClass = - T extends Trait - ? C - : never - -/** - * Returns the instance type of a trait. - * @template T - The trait type. - */ -export type TraitInstance = - T extends Trait - ? InstanceType - : never - - -/** - * Creates a trait using the provided trait applier function. - * @template C - The abstract class type. - * @param applier - The trait applier function. - * @returns A trait. - * @example - * Creates a trait: - * ```ts - * const Permissible = trait(Parent => { - * abstract class Permissible extends Parent { - * static readonly defaultPermissions: string[] = [] - * permissions: string[] = [] - * - * // Constructor is optional - * // If you wish to use it, make sure it takes any[] as an args array and passes it to the super call. This is necessary for inheritance to work properly. - * // Trait constructors cannot have typed arguments of their own, they only serve to run logic during object instantiation. - * constructor(...args: any[]) { - * super(...args) - * } - * } - * - * return Permissible - * }) - * ``` - * Creates a generic trait: - * ```ts - * const Identifiable = () => - * trait(Parent => { - * abstract class Identifiable extends Parent { - * abstract readonly id: ID - * - * equals(el: Identifiable) { - * return this.id === el.id - * } - * - * // Optional - * constructor(...args: any[]) { - * super(...args) - * } - * } - * - * return Identifiable - * }) - * ``` - * Creates a subtrait: - * ```ts - * const ImplementsIdentifiable = (defaultID: ID) => - * trait(Parent => { - * abstract class ImplementsIdentifiable extends extendsAndExpresses( - * Parent, - * Identifiable(), - * ) { - * id: ID = defaultID - * - * // Optional - * constructor(...args: any[]) { - * super(...args) - * } - * } - * - * return ImplementsIdentifiable - * }) - * ``` - */ -export function trait< - C extends AbstractClass ->( - applier: TraitApplier -) { - return applier as Trait -} - - -/** - * Extends a class with the given traits and expresses their combined functionality. - * @template C - The abstract class type. - * @template Traits - An array of traits. - * @param extend - The class to extend. - * @param traits - An array of traits to apply. - * @returns A new class type expressing the combined functionality of the base class and traits. - * @example - * Extends a superclass and applies traits: - * ```ts - * class User extends extendsAndExpresses(Entity, - * Identifiable(), - * Permissible, - * ) { - * readonly id: bigint - * - * constructor(id: bigint) { - * super() - * this.id = id - * } - * } - * ``` - */ -export function extendsAndExpresses< - C extends AbstractClass, - Traits extends readonly Trait[], ->( - extend: C, - ...traits: Traits -) { - return traits.reduce( - (previous, trait) => trait(previous), - extend, - ) as ( - AbstractClass< - InstanceType & - UnionToIntersection< - TraitInstance< - Traits[number] - > - >, - - ConstructorParameters - > & - - StaticMembers & - StaticMembers< - UnionToIntersection< - TraitClass< - Traits[number] - > - > - > - ) -} - -/** - * Expresses the combined functionality of multiple traits. - * @template Traits - An array of trait. - * @param traits - An array of trait to apply. - * @returns A new class type expressing the combined functionality of the traits. - * @example - * Applies traits to a class: - * ```ts - * class User extends expresses(Identifiable(), Permissible) { - * readonly id: bigint - * - * constructor(id: bigint) { - * super() - * this.id = id - * } - * } - * ``` - */ -export function expresses< - Traits extends readonly Trait[], ->( - ...traits: Traits -) { - return extendsAndExpresses(Object, ...traits) -} +export * from "./expresses" +export * from "./trait" diff --git a/src/legacy/tests.ts b/src/legacy/tests.ts deleted file mode 100644 index e86528d..0000000 --- a/src/legacy/tests.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { AbstractClass } from "type-fest" -import { expresses } from "./trait" - - -function inspectClass(class_: AbstractClass) { - Object.getOwnPropertyNames(class_).forEach(name => { - console.log( - "[static]", - name, - Object.getOwnPropertyDescriptor(class_, name) - ) - }) - - Object.getOwnPropertyNames(class_.prototype).forEach(name => { - console.log( - "[prototype]", - name, - Object.getOwnPropertyDescriptor(class_.prototype, name) - ) - }) -} - - -abstract class Identified { - abstract id: ID - - equals(el: Identified) { - return this.id === el.id - } - - // initializer() { - // console.log("Identified initializer") - // } -} - -class ImplementsIdentifiable extends Identified { - id!: ID -} - - -abstract class Permissible { - static readonly defaultPermissions: string[] = [] - permissions: string[] = [] - // permissions!: string[] - - constructor() { - console.log("Permissible constructor") - } - - initializer() { - console.log("Permissible initializer") - this.permissions = [] - } -} - - -class User extends expresses( - Identified as typeof Identified, - // Identified, - Permissible, -) { - readonly id: bigint - - constructor(id: bigint) { - super() - this.id = id - } -} - -const user1 = new User(BigInt(1)) -const user2 = new User(BigInt(2)) - -console.log(user1) -console.log(user1.equals(user2)) diff --git a/src/legacy/trait.ts b/src/legacy/trait.ts deleted file mode 100644 index 10cc87b..0000000 --- a/src/legacy/trait.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { AbstractClass, Class, UnionToIntersection } from "type-fest" -import { StaticMembers, copyProperties, getInheritanceHierarchy } from "./util/class" - - -/** - * Represents a trait that can be used to define common behavior - * for classes and abstract classes. - * @typeParam T - The type of the trait. - */ -export type Trait = - AbstractClass - - -/** - * Creates a link class that expresses the given traits. - * @param traits - An array of traits to be expressed by the link class. - * @returns A dynamically created class that expresses the given traits. - * @typeParam Traits - An array of traits that the link class expresses. - */ -export function expresses< - Traits extends readonly Trait[] ->( - ...traits: Traits -) { - return makeLinkClass(traits) -} - - -/** - * Creates a link class that extends a base class and expresses the given traits. - * @param extend - The base class or abstract class to extend. - * @param traits - An array of traits to be expressed by the link class. - * @returns A dynamically created class that extends the given base class and expresses the given traits. - * @typeParam C - The type of the base class to extend. - * @typeParam Traits - An array of traits that the link class expresses. - */ -export function extendsAndExpresses< - C extends Class - | AbstractClass, - Traits extends readonly Trait[], ->( - extend: C, - ...traits: Traits -) { - return makeLinkClass(traits, extend) -} - - -/** - * Creates a link class that expresses the given traits and optionally extends a base class. - * @param traits - An array of traits to be expressed by the link class. - * @param extend - The base class or abstract class to extend (optional). - * @returns A dynamically created class that expresses the given traits and extends the base class. - * @typeParam Traits - An array of traits that the link class expresses. - * @typeParam C - The type of the base class to extend (optional). - */ -export function makeLinkClass< - Traits extends readonly Trait[], - C extends Class - | AbstractClass - | undefined = undefined, ->( - traits: Traits, - extend?: C, -) { - const class_ = extend - ? class extends extend { - constructor(...args: any[]) { - super(...args) - - traits.forEach(trait => { - trait.prototype.initializer?.call(this) - }) - } - } - : class { - constructor() { - traits.forEach(trait => { - trait.prototype.initializer?.call(this) - }) - } - } - - traits.forEach(trait => { - getInheritanceHierarchy(trait).forEach(current => { - copyProperties( - current, - class_, - ["name", "length"], - ["constructor"], - ) - }) - }) - - return class_ as unknown as ( - (C extends Class | AbstractClass - ? ( - AbstractClass< - InstanceType & - UnionToIntersection< - InstanceType< - Traits[number] - > - >, - - ConstructorParameters - > & - - StaticMembers - ) - : Trait< - UnionToIntersection< - InstanceType< - Traits[number] - > - > - > - ) & - - StaticMembers< - UnionToIntersection< - Traits[number] - > - > - ) -} diff --git a/src/legacy/util/class.ts b/src/legacy/util/class.ts deleted file mode 100644 index 518c960..0000000 --- a/src/legacy/util/class.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { AbstractClass } from "type-fest" - - -/** - * Represents the static members of a class. - * - * @template C - The type of the class for which static members are extracted. - * @typeparam The static members of the class. - */ -export type StaticMembers = Pick - - -/** - * Flattens the inheritance hierarchy of a given class by copying all properties - * from its superclass chain into a single object. - * - * @template C - The type of the class to be flattened, extending AbstractClass. - * @param C - The class to be flattened. - * @returns A new class with properties flattened from the entire inheritance hierarchy. - */ -export function flattenClass< - C extends AbstractClass ->(class_: C) { - const flattenedClass = class {} as unknown as C - - getInheritanceHierarchy(class_).forEach(current => { - copyProperties(current, flattenedClass) - }) - - copyProperty(class_, flattenedClass, "name") - copyProperty(class_.prototype, flattenedClass.prototype, "constructor") - - return flattenedClass -} - - -/** - * Retrieves the inheritance hierarchy of a given class, including itself. - * - * @param class_ - The class for which the inheritance hierarchy is retrieved. - * @returns An array representing the inheritance hierarchy, ordered from the furthest in the hierarchy to `class_` itself. - */ -export function getInheritanceHierarchy( - class_: AbstractClass -): AbstractClass[] { - const parent = Object.getPrototypeOf(class_) - - return isClass(parent) - ? [...getInheritanceHierarchy(parent), class_] - : [class_] -} - - -/** - * Checks if a given element appears to be a class based on its string representation. - * - * @param el - The element to check for being a class. - * @returns `true` if the element is likely a class; otherwise, `false`. - */ -export function isClass(el: { toString: () => string }) { - return Boolean(el.toString().match(/^class(?: [.\S]+)?(?: extends [.\S]+)? {[\s\S]*}$/)) -} - - -/** - * Copies properties from one class to another, including static and prototype properties. - * - * @param from - The source class to copy properties from. - * @param to - The destination class to copy properties to. - */ -export function copyProperties( - from: AbstractClass, - to: AbstractClass, - ignoreKeys: string[] = [], - ignorePrototypeKeys: string[] = [], -) { - Object.getOwnPropertyNames(from).forEach(name => { - if (name === "prototype" - || ignoreKeys.find(v => v === name) - ) - return - - // console.log(from, to, name, Object.getOwnPropertyDescriptor(from, name)) - - copyProperty(from, to, name) - }) - - Object.getOwnPropertyNames(from.prototype).forEach(name => { - if (ignorePrototypeKeys.find(v => v === name)) - return - - // console.log(from, to, name, Object.getOwnPropertyDescriptor(from, name)) - - copyProperty(from.prototype, to.prototype, name) - }) -} - - -export function copyProperty( - from: unknown, - to: unknown, - name: string, -) { - Object.defineProperty( - to, - name, - Object.getOwnPropertyDescriptor(from, name) || Object.create(null), - ) -} diff --git a/src/tests.ts b/src/tests.ts index 6bd14c4..d61e2d9 100644 --- a/src/tests.ts +++ b/src/tests.ts @@ -1,9 +1,11 @@ +import { AbstractClass } from "type-fest" import { expresses, extendsAndExpresses, trait } from "." +import { ClassesInstances, MergeInheritanceTree } from "./util" const Identifiable = () => - trait(Parent => { - abstract class Identifiable extends Parent { + trait(Super => { + abstract class Identifiable extends Super { abstract readonly id: ID equals(el: Identifiable) { @@ -20,9 +22,9 @@ const Identifiable = () => }) const ImplementsIdentifiable = (defaultID: ID) => - trait(Parent => { + trait(Super => { abstract class ImplementsIdentifiable extends extendsAndExpresses( - Parent, + Super, Identifiable(), ) { id: ID = defaultID @@ -37,8 +39,8 @@ const ImplementsIdentifiable = (defaultID: ID) => }) -const Permissible = trait(Parent => { - abstract class Permissible extends Parent { +const Permissible = trait(Super => { + abstract class Permissible extends Super { static readonly defaultPermissions: string[] = [] permissions: string[] = [] @@ -68,3 +70,51 @@ class User extends UserProto { const user1 = new User(1n) console.log(user1) console.log(user1.equals(user1)) + + +const Test1 = trait(Super => { + abstract class Test1 extends Super { + declare static name: string + declare static testValue: ( + { _tag: "type1", value: string } | + { _tag: "type2", value: number } + ) + + abstract name: string + + declare createdAt: Date + declare readonly status: ( + { _tag: "awaitingPayment" } | + { _tag: "active", activeSince: Date, expiresAt?: Date } | + { _tag: "expired", expiredSince: Date } + ) + } + + return Test1 +}) + +const Test2 = trait(Super => { + abstract class Test2 extends Super { + declare readonly status: { _tag: "active", activeSince: Date, expiresAt?: Date } + } + + return Test2 +}) + +const Test3 = trait(Super => { + abstract class Test3 extends Super { + declare static testValue: { _tag: "type2", value: number } + declare lol: 10n + } + + return Test3 +}) + +const TestObjectProto = expresses(Test1, Test2, Test3) +TestObjectProto.testValue + +type CleanupInheritanceTreeProperties[]> = ( + MergeInheritanceTree< + ClassesInstances + > +) diff --git a/src/trait.ts b/src/trait.ts new file mode 100644 index 0000000..21fd00c --- /dev/null +++ b/src/trait.ts @@ -0,0 +1,117 @@ +import { AbstractClass, AbstractConstructor, Opaque } from "type-fest" + + +/** + * Represents a trait that can be applied to a class. + * @template C - The abstract class type. + */ +export type Trait< + C extends AbstractClass +> = Opaque< + TraitApplier, + "@thilawyn/thilatrait/Trait" +> + +export type TraitApplierSuperTag = "@thilawyn/thilatrait/Super" + +/** + * Represents the function signature for applying a trait to a parent class. + * @template C - The abstract class type. + */ +export type TraitApplier< + C extends AbstractClass +> = ( + (Super: Opaque, TraitApplierSuperTag>) => Opaque +) + +/** + * Creates a trait using the provided trait applier function. + * @template C - The abstract class type. + * @param applier - The trait applier function. + * @returns A trait. + * @example + * Creates a trait: + * ```ts + * const Permissible = trait(Super => { + * abstract class Permissible extends Super { + * static readonly defaultPermissions: string[] = [] + * permissions: string[] = [] + * + * // Constructor is optional + * // If you wish to use it, make sure it takes any[] as an args array and passes it to the super call. This is necessary for inheritance to work properly. + * // Trait constructors cannot have typed arguments of their own, they only serve to run logic during object instantiation. + * constructor(...args: any[]) { + * super(...args) + * } + * } + * + * return Permissible + * }) + * ``` + * Creates a generic trait: + * ```ts + * const Identifiable = () => + * trait(Super => { + * abstract class Identifiable extends Super { + * abstract readonly id: ID + * + * equals(el: Identifiable) { + * return this.id === el.id + * } + * + * // Optional + * constructor(...args: any[]) { + * super(...args) + * } + * } + * + * return Identifiable + * }) + * ``` + * Creates a subtrait: + * ```ts + * const ImplementsIdentifiable = (defaultID: ID) => + * trait(Super => { + * abstract class ImplementsIdentifiable extends extendsAndExpresses( + * Super, + * Identifiable(), + * ) { + * id: ID = defaultID + * + * // Optional + * constructor(...args: any[]) { + * super(...args) + * } + * } + * + * return ImplementsIdentifiable + * }) + * ``` + */ +export function trait< + C extends AbstractClass +>( + applier: TraitApplier +) { + return applier as Trait +} + +/** + * Returns the class type of a trait. + * @template T - The trait type. + */ +export type TraitClass = ( + T extends Trait + ? C + : never +) + +/** + * Returns the instance type of a trait. + * @template T - The trait type. + */ +export type TraitInstance = ( + T extends Trait + ? InstanceType + : never +) diff --git a/src/util/class.ts b/src/util/class.ts new file mode 100644 index 0000000..325aba5 --- /dev/null +++ b/src/util/class.ts @@ -0,0 +1,66 @@ +import { AbstractClass } from "type-fest" + + +/** + * Represents an array of instances corresponding to the provided classes. + * @template Classes - An array of classes extending AbstractClass. + */ +export type ClassesInstances[]> = ( + Classes extends [infer Class, ...infer Rest] + ? Class extends AbstractClass + ? Rest extends AbstractClass[] + ? [InstanceType, ...ClassesInstances] + : never + : never + : [] +) + +/** + * Represents an intersection of instances of the provided classes. + * @template Classes - An array of classes extending AbstractClass. + */ +export type ClassesInstancesIntersection[]> = ( + Classes extends [infer Class, ...infer Rest] + ? Class extends AbstractClass + ? Rest extends AbstractClass[] + ? InstanceType & ClassesInstancesIntersection + : never + : never + : {} +) + +/** + * Represents the static members of a class. + * @template Class - A class extending AbstractClass. + */ +export type StaticMembers> = ( + Omit +) + +/** + * Represents an array of static members corresponding to the provided classes. + * @template Classes - An array of classes extending AbstractClass. + */ +export type ClassesStaticMembers[]> = ( + Classes extends [infer Class, ...infer Rest] + ? Class extends AbstractClass + ? Rest extends AbstractClass[] + ? [StaticMembers, ...ClassesStaticMembers] + : never + : never + : [] +) + +/** + * Represents an intersection of static members of the provided classes. + * @template Classes - An array of classes extending AbstractClass. + */ +export type ClassesStaticMembersIntersection[]> = ( + Classes extends [infer Class, ...infer Rest] + ? Class extends AbstractClass + ? Rest extends AbstractClass[] + ? StaticMembers & ClassesStaticMembersIntersection + : never + : never + : {} +) diff --git a/src/util/index.ts b/src/util/index.ts new file mode 100644 index 0000000..c5faea6 --- /dev/null +++ b/src/util/index.ts @@ -0,0 +1,3 @@ +export * from "./class" +export * from "./inheritance" +export * from "./trait" diff --git a/src/util/inheritance.ts b/src/util/inheritance.ts new file mode 100644 index 0000000..a179d78 --- /dev/null +++ b/src/util/inheritance.ts @@ -0,0 +1,40 @@ +/** + * Represents the common keys between two types. + * @template A - The first type. + * @template B - The second type. + */ +export type CommonKeys = Extract + +/** + * Merges an inheritance tree defined by an array of types, considering overrides. + * @template T - An array of types representing the inheritance tree. + */ +export type MergeInheritanceTree = ( + T extends [infer Super, infer Self, ...infer Rest] + ? Pick> extends Pick> + ? MergeInheritanceTree<[ + Omit> & Self, + ...Rest, + ]> + : never + : T extends [infer Self] + ? Self + : void +) + +/** + * Merges an inheritance tree defined by an array of types without allowing overrides. + * @template T - An array of types representing the inheritance tree. + */ +export type MergeInheritanceTreeWithoutOverriding = ( + T extends [infer Super, infer Self, ...infer Rest] + ? Pick> extends Pick> + ? MergeInheritanceTreeWithoutOverriding<[ + Super & Self, + ...Rest, + ]> + : never + : T extends [infer Self] + ? Self + : void +) diff --git a/src/util/trait.ts b/src/util/trait.ts new file mode 100644 index 0000000..149599d --- /dev/null +++ b/src/util/trait.ts @@ -0,0 +1,16 @@ +import { Trait, TraitClass } from ".." + + +/** + * Represents an array of classes corresponding to the provided traits. + * @template Traits - An array of traits extending Trait. + */ +export type TraitsClasses[]> = ( + Traits extends [infer T, ...infer Rest] + ? T extends Trait + ? Rest extends Trait[] + ? [TraitClass, ...TraitsClasses] + : never + : never + : [] +)