From 47907c67b7f2ea19e9f0caf285cba76008892245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Thu, 18 Jan 2024 18:35:02 +0100 Subject: [PATCH 1/8] TraitApplierTag --- src/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0d273bc..c1ed0f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,9 +18,11 @@ export type Trait< C extends AbstractClass > = Opaque< TraitApplier, - "thilatrait/Trait" + "@thilawyn/thilatrait/Trait" > +export type TraitApplierTag = "@thilawyn/thilatrait/Trait" + /** * Represents the function signature for applying a trait to a parent class. * @template C - The abstract class type. @@ -28,7 +30,7 @@ export type Trait< export type TraitApplier< C extends AbstractClass > = - (Parent: AbstractConstructor) => C + (Parent: Opaque, TraitApplierTag>) => Opaque /** * Returns the class type of a trait. @@ -154,7 +156,7 @@ export function extendsAndExpresses< ) { return traits.reduce( (previous, trait) => trait(previous), - extend, + extend as Opaque, ) as ( AbstractClass< InstanceType & -- 2.49.1 From 69ecd95cbb481b65893b91827305465eaac49270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 27 Jan 2024 00:16:43 +0100 Subject: [PATCH 2/8] Recursive extend type work --- src/index.ts | 26 ++++++++++++++++++++++- src/tests.ts | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index c1ed0f4..e58715f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,7 @@ export type Trait< "@thilawyn/thilatrait/Trait" > -export type TraitApplierTag = "@thilawyn/thilatrait/Trait" +export type TraitApplierTag = "@thilawyn/thilatrait/Parent" /** * Represents the function signature for applying a trait to a parent class. @@ -154,6 +154,30 @@ export function extendsAndExpresses< extend: C, ...traits: Traits ) { + // Combine all static members of the traits + type AllStaticMembers = UnionToIntersection> + + // Combine all instance members of the traits + type AllInstanceMembers = UnionToIntersection> + + // Extract common keys from all traits + type CommonKeys = keyof AllStaticMembers & keyof AllInstanceMembers + + // Exclude common keys from the static members of the last trait + type StaticMembersWithoutCommon = Omit + + // Exclude common keys from the instance members of the last trait + type InstanceMembersWithoutCommon = Omit + + // Combine the instance members of the base class and the last trait + type CombinedInstanceMembers = InstanceType & InstanceMembersWithoutCommon + + // Combine the static members of the base class and the last trait + type CombinedStaticMembers = StaticMembers & StaticMembersWithoutCommon + + // Combine the instance and static members + type CombinedMembers = CombinedInstanceMembers & CombinedStaticMembers + return traits.reduce( (previous, trait) => trait(previous), extend as Opaque, diff --git a/src/tests.ts b/src/tests.ts index 6bd14c4..7bfdb96 100644 --- a/src/tests.ts +++ b/src/tests.ts @@ -1,4 +1,5 @@ -import { expresses, extendsAndExpresses, trait } from "." +import { AbstractClass, Class, Simplify } from "type-fest" +import { TraitInstance, expresses, extendsAndExpresses, trait } from "." const Identifiable = () => @@ -68,3 +69,58 @@ class User extends UserProto { const user1 = new User(1n) console.log(user1) console.log(user1.equals(user1)) + + +abstract class TestTrait1 { + abstract variable: number + abstract member: { + issou: string + } +} + +abstract class TestTrait2 { + abstract lol: BigInt + abstract member: { + issou: "gneugneu", + adolf: string + } +} + + +type StaticMembers = { + [Key in keyof C as Key extends "prototype" ? never : Key]: C[Key] +} + +type CommonKeys = Extract + +type Extend2 = ( + Pick> extends Pick> + ? Omit> & Self + : never +) + +type ExtendN = ( + Classes extends [infer Parent, infer Self, ...infer Rest] + ? Extend2 & ExtendN + : Classes extends [infer Self, ...infer Others] + ? Self + : "Empty array" +) + +// type ExtendClass< +// Self extends AbstractClass, +// Parent extends AbstractClass, +// > = ( + +// ) + +type Test = Simplify> +type MixedStaticTestTraits = ExtendN<[ + StaticMembers, + StaticMembers, +]> + +type MixedTestTraitsProto = Class< + Extend2, + any[] +> & Extend2, StaticMembers> -- 2.49.1 From e5fbee56edb477b2586fb7d4cf7bd0da321f0acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 27 Jan 2024 02:20:56 +0100 Subject: [PATCH 3/8] New implementation tests --- src/index.ts | 24 ------------ src/tests.ts | 106 ++++++++++++++++++++++++++++++++------------------- 2 files changed, 67 insertions(+), 63 deletions(-) diff --git a/src/index.ts b/src/index.ts index e58715f..ea9c1b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -154,30 +154,6 @@ export function extendsAndExpresses< extend: C, ...traits: Traits ) { - // Combine all static members of the traits - type AllStaticMembers = UnionToIntersection> - - // Combine all instance members of the traits - type AllInstanceMembers = UnionToIntersection> - - // Extract common keys from all traits - type CommonKeys = keyof AllStaticMembers & keyof AllInstanceMembers - - // Exclude common keys from the static members of the last trait - type StaticMembersWithoutCommon = Omit - - // Exclude common keys from the instance members of the last trait - type InstanceMembersWithoutCommon = Omit - - // Combine the instance members of the base class and the last trait - type CombinedInstanceMembers = InstanceType & InstanceMembersWithoutCommon - - // Combine the static members of the base class and the last trait - type CombinedStaticMembers = StaticMembers & StaticMembersWithoutCommon - - // Combine the instance and static members - type CombinedMembers = CombinedInstanceMembers & CombinedStaticMembers - return traits.reduce( (previous, trait) => trait(previous), extend as Opaque, diff --git a/src/tests.ts b/src/tests.ts index 7bfdb96..ac733c4 100644 --- a/src/tests.ts +++ b/src/tests.ts @@ -1,5 +1,5 @@ -import { AbstractClass, Class, Simplify } from "type-fest" -import { TraitInstance, expresses, extendsAndExpresses, trait } from "." +import { AbstractClass } from "type-fest" +import { expresses, extendsAndExpresses, trait } from "." const Identifiable = () => @@ -71,56 +71,84 @@ console.log(user1) console.log(user1.equals(user1)) -abstract class TestTrait1 { - abstract variable: number - abstract member: { - issou: string - } +abstract class Test1 { + declare static name: string + abstract name: string + + declare createdAt: Date + declare readonly status: ( + { _tag: "awaitingPayment" } | + { _tag: "active", activeSince: Date, expiresAt?: Date } | + { _tag: "expired", expiredSince: Date } + ) } -abstract class TestTrait2 { - abstract lol: BigInt - abstract member: { - issou: "gneugneu", - adolf: string - } +class Test2 { + declare readonly status: { _tag: "active", activeSince: Date, expiresAt?: Date } } - -type StaticMembers = { - [Key in keyof C as Key extends "prototype" ? never : Key]: C[Key] +class Test3 { + declare lol: 10n } + type CommonKeys = Extract -type Extend2 = ( - Pick> extends Pick> - ? Omit> & Self - : never -) - -type ExtendN = ( - Classes extends [infer Parent, infer Self, ...infer Rest] - ? Extend2 & ExtendN - : Classes extends [infer Self, ...infer Others] +type Extend = ( + T extends [infer Super, infer Self, ...infer Rest] + ? Pick> extends Pick> + ? Extend<[ + Omit> & Self, + ...Rest, + ]> + : never + : T extends [infer Self] ? Self : "Empty array" ) -// type ExtendClass< -// Self extends AbstractClass, -// Parent extends AbstractClass, -// > = ( +type ClassesInstances = ( + Classes extends [infer Class, ...infer Rest] + ? Class extends AbstractClass + ? [InstanceType, ...ClassesInstances] + : never + : [] +) -// ) +type StaticMembers> = ( + Omit +) -type Test = Simplify> -type MixedStaticTestTraits = ExtendN<[ - StaticMembers, - StaticMembers, +type ClassesStaticMembers = ( + Classes extends [infer Class, ...infer Rest] + ? Class extends AbstractClass + ? [StaticMembers, ...ClassesStaticMembers] + : never + : [] +) + +type ExtendClasses[]> = ( + AbstractClass< + Extend> + > & + Extend> +) + + +type MixedTestProto = ExtendClasses<[ + typeof Test1, + typeof Test2, + typeof Test3, ]> -type MixedTestTraitsProto = Class< - Extend2, - any[] -> & Extend2, StaticMembers> +declare const MixedTestProto: MixedTestProto + +class MixedTest extends MixedTestProto {} + +MixedTest.prototype +new MixedTest().name + +// type MixedTestTraitsProto = Class< +// Extend2, +// any[] +// > & Extend2, StaticMembers> -- 2.49.1 From 92645bfd6b782c33eb0a3d0df13376f4089b3508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 27 Jan 2024 18:45:55 +0100 Subject: [PATCH 4/8] Inheritance util --- src/tests.ts | 53 ++++++++------------------------- src/util/class.ts | 66 +++++++++++++++++++++++++++++++++++++++++ src/util/index.ts | 2 ++ src/util/inheritance.ts | 40 +++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 41 deletions(-) create mode 100644 src/util/class.ts create mode 100644 src/util/index.ts create mode 100644 src/util/inheritance.ts diff --git a/src/tests.ts b/src/tests.ts index ac733c4..87609f0 100644 --- a/src/tests.ts +++ b/src/tests.ts @@ -1,5 +1,6 @@ -import { AbstractClass } from "type-fest" +import { AbstractClass, UnionToIntersection } from "type-fest" import { expresses, extendsAndExpresses, trait } from "." +import { ClassesInstances, ClassesInstancesIntersection, ClassesStaticMembers, MergeInheritanceTree, MergeInheritanceTreeWithoutOverriding } from "./util" const Identifiable = () => @@ -92,46 +93,16 @@ class Test3 { } -type CommonKeys = Extract - -type Extend = ( - T extends [infer Super, infer Self, ...infer Rest] - ? Pick> extends Pick> - ? Extend<[ - Omit> & Self, - ...Rest, - ]> - : never - : T extends [infer Self] - ? Self - : "Empty array" -) - -type ClassesInstances = ( - Classes extends [infer Class, ...infer Rest] - ? Class extends AbstractClass - ? [InstanceType, ...ClassesInstances] - : never - : [] -) - -type StaticMembers> = ( - Omit -) - -type ClassesStaticMembers = ( - Classes extends [infer Class, ...infer Rest] - ? Class extends AbstractClass - ? [StaticMembers, ...ClassesStaticMembers] - : never - : [] -) - type ExtendClasses[]> = ( AbstractClass< - Extend> + // Extend> + MergeInheritanceTreeWithoutOverriding< + ClassesInstances + > > & - Extend> + MergeInheritanceTree< + ClassesStaticMembers + > ) @@ -140,13 +111,13 @@ type MixedTestProto = ExtendClasses<[ typeof Test2, typeof Test3, ]> - declare const MixedTestProto: MixedTestProto -class MixedTest extends MixedTestProto {} +class MixedTest extends MixedTestProto { +} MixedTest.prototype -new MixedTest().name +new MixedTest().status.expiresAt // type MixedTestTraitsProto = Class< // Extend2, 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..05e8a4a --- /dev/null +++ b/src/util/index.ts @@ -0,0 +1,2 @@ +export * from "./class" +export * from "./inheritance" 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 +) -- 2.49.1 From 1722372a48375fd828af017afd70d23444d6dfb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 27 Jan 2024 22:23:56 +0100 Subject: [PATCH 5/8] Refactoring --- src/expresses.ts | 84 +++++++++++++++++++ src/index.ts | 209 +--------------------------------------------- src/tests.ts | 33 +------- src/trait.ts | 117 ++++++++++++++++++++++++++ src/util/index.ts | 1 + src/util/trait.ts | 12 +++ 6 files changed, 220 insertions(+), 236 deletions(-) create mode 100644 src/expresses.ts create mode 100644 src/trait.ts create mode 100644 src/util/trait.ts 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 ea9c1b5..ae86066 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,207 +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, - "@thilawyn/thilatrait/Trait" -> - -export type TraitApplierTag = "@thilawyn/thilatrait/Parent" - -/** - * 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: Opaque, TraitApplierTag>) => Opaque - -/** - * 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 Opaque, - ) 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/tests.ts b/src/tests.ts index 87609f0..2930e45 100644 --- a/src/tests.ts +++ b/src/tests.ts @@ -1,6 +1,6 @@ -import { AbstractClass, UnionToIntersection } from "type-fest" +import { AbstractClass } from "type-fest" import { expresses, extendsAndExpresses, trait } from "." -import { ClassesInstances, ClassesInstancesIntersection, ClassesStaticMembers, MergeInheritanceTree, MergeInheritanceTreeWithoutOverriding } from "./util" +import { ClassesInstances, MergeInheritanceTree } from "./util" const Identifiable = () => @@ -93,33 +93,8 @@ class Test3 { } -type ExtendClasses[]> = ( - AbstractClass< - // Extend> - MergeInheritanceTreeWithoutOverriding< - ClassesInstances - > - > & +type CleanupInheritanceTreeProperties[]> = ( MergeInheritanceTree< - ClassesStaticMembers + ClassesInstances > ) - - -type MixedTestProto = ExtendClasses<[ - typeof Test1, - typeof Test2, - typeof Test3, -]> -declare const MixedTestProto: MixedTestProto - -class MixedTest extends MixedTestProto { -} - -MixedTest.prototype -new MixedTest().status.expiresAt - -// type MixedTestTraitsProto = Class< -// Extend2, -// any[] -// > & Extend2, StaticMembers> 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/index.ts b/src/util/index.ts index 05e8a4a..c5faea6 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -1,2 +1,3 @@ export * from "./class" export * from "./inheritance" +export * from "./trait" diff --git a/src/util/trait.ts b/src/util/trait.ts new file mode 100644 index 0000000..97a2571 --- /dev/null +++ b/src/util/trait.ts @@ -0,0 +1,12 @@ +import { Trait, TraitClass } from ".." + + +export type TraitsClasses[]> = ( + Traits extends [infer T, ...infer Rest] + ? T extends Trait + ? Rest extends Trait[] + ? [TraitClass, ...TraitsClasses] + : never + : never + : [] +) -- 2.49.1 From f2e64ac681551dce9c3414993decd0d4b7fab55c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 27 Jan 2024 23:26:40 +0100 Subject: [PATCH 6/8] Tests --- src/tests.ts | 64 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/src/tests.ts b/src/tests.ts index 2930e45..d61e2d9 100644 --- a/src/tests.ts +++ b/src/tests.ts @@ -4,8 +4,8 @@ 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) { @@ -22,9 +22,9 @@ const Identifiable = () => }) const ImplementsIdentifiable = (defaultID: ID) => - trait(Parent => { + trait(Super => { abstract class ImplementsIdentifiable extends extendsAndExpresses( - Parent, + Super, Identifiable(), ) { id: ID = defaultID @@ -39,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[] = [] @@ -72,26 +72,46 @@ console.log(user1) console.log(user1.equals(user1)) -abstract class Test1 { - declare static name: string - abstract name: string +const Test1 = trait(Super => { + abstract class Test1 extends Super { + declare static name: string + declare static testValue: ( + { _tag: "type1", value: string } | + { _tag: "type2", value: number } + ) - declare createdAt: Date - declare readonly status: ( - { _tag: "awaitingPayment" } | - { _tag: "active", activeSince: Date, expiresAt?: Date } | - { _tag: "expired", expiredSince: Date } - ) -} + abstract name: string -class Test2 { - declare readonly status: { _tag: "active", activeSince: Date, expiresAt?: Date } -} + declare createdAt: Date + declare readonly status: ( + { _tag: "awaitingPayment" } | + { _tag: "active", activeSince: Date, expiresAt?: Date } | + { _tag: "expired", expiredSince: Date } + ) + } -class Test3 { - declare lol: 10n -} + 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< -- 2.49.1 From 9b9b0ea846c4db30c54fbaa6331196f6175b15a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 27 Jan 2024 23:32:54 +0100 Subject: [PATCH 7/8] Cleanup --- src/legacy/tests.ts | 74 ----------------------- src/legacy/trait.ts | 126 --------------------------------------- src/legacy/util/class.ts | 109 --------------------------------- src/util/trait.ts | 4 ++ 4 files changed, 4 insertions(+), 309 deletions(-) delete mode 100644 src/legacy/tests.ts delete mode 100644 src/legacy/trait.ts delete mode 100644 src/legacy/util/class.ts 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/util/trait.ts b/src/util/trait.ts index 97a2571..149599d 100644 --- a/src/util/trait.ts +++ b/src/util/trait.ts @@ -1,6 +1,10 @@ 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 -- 2.49.1 From deb9a28c36184f2f27b3511a90b28f643bc8d3a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 27 Jan 2024 23:36:57 +0100 Subject: [PATCH 8/8] Version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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/" -- 2.49.1