diff --git a/README.md b/README.md
index 7817ffd..cba05c5 100644
--- a/README.md
+++ b/README.md
@@ -62,7 +62,7 @@ const dataset_x = new N3.Store(new N3.Parser().parse(rdf)) // rdf holds the turt
Class usage:
```javascript
-const person1 = new Person("https://example.org/person1", dataset_x, DataFactory)
+const person1 = Person.from("https://example.org/person1", dataset_x, DataFactory)
// Get property
console.log(person1.name)
@@ -154,7 +154,7 @@ ex:person2
Class usage:
```javascript
-const person2 = new Person("https://example.org/person2", dataset_z, DataFactory)
+const person2 = Person.from("https://example.org/person2", dataset_z, DataFactory)
// Get property
console.log(person2.name)
@@ -165,7 +165,7 @@ console.log(person2.mum.name)
// outputs "Alice"
// Set class properties
-const person3 = new Person("https://example.org/person3", dataset_z, DataFactory)
+const person3 = Person.from("https://example.org/person3", dataset_z, DataFactory)
person3.name = "Joanne"
person1.mum = person3
console.log(person1.mum.name)
diff --git a/src/TermWrapper.ts b/src/TermWrapper.ts
index 192274f..b48bce6 100644
--- a/src/TermWrapper.ts
+++ b/src/TermWrapper.ts
@@ -98,6 +98,111 @@ export class TermWrapper implements IRdfJsTerm {
this._factory = factory
}
+ //#region Static factory
+
+ /**
+ * Creates a new instance of this class (or subclass) from an IRI, typed as both the wrapper and the {@link NamedNode} it wraps.
+ *
+ * @remarks
+ * This factory is equivalent to invoking the constructor directly, differing only in the return type: the constructor returns the declared instance type, whereas this factory returns the intersection of the (sub)class instance type and {@link NamedNode}. Because the result _is_ a {@link NamedNode} at the type level, it can be passed directly anywhere an RDF/JS {@link Term} is expected — for example when creating quads with a {@link DataFactory} or when matching with {@link DatasetCore.match} — without casts.
+ *
+ * @example Creating an instance of a subclass
+ * Assume the following RDF data:
+ * ```turtle
+ * BASE
+ *
+ * "some value" .
+ * ```
+ * We can wrap the subject and use the wrapper directly as a named node:
+ * ```ts
+ * class SomeClass extends TermWrapper {
+ * get someProperty(): string {
+ * return RequiredFrom.subjectPredicate(this, "http://example.com/someProperty", LiteralAs.string)
+ * }
+ * }
+ *
+ * const instance = SomeClass.from("http://example.com/someSubject", dataset, factory)
+ * // instance is typed as SomeClass & NamedNode
+ *
+ * const value = instance.someProperty // contains "some value"
+ * ```
+ *
+ * @example Using created instances directly as RDF/JS terms
+ * In contrast to instances created with the constructor, no casts are required to use the result as a term:
+ * ```ts
+ * const instance = SomeClass.from("http://example.com/someSubject", dataset, factory)
+ *
+ * // Used as subject when creating a quad
+ * factory.quad(instance, predicate, object)
+ *
+ * // Used as subject when matching statements in a dataset
+ * dataset.match(instance)
+ * ```
+ *
+ * @param term The IRI of a named node that is the original term being wrapped.
+ * @param dataset The dataset that contains the term being wrapped.
+ * @param factory A collection of methods for creating terms.
+ * @returns A new instance of the class this method was invoked on, typed additionally as the {@link NamedNode} it wraps.
+ *
+ * @see
+ * - [Named nodes in the RDF/JS Data model specification](https://rdf.js.org/data-model-spec/#namednode-interface)
+ */
+ public static from any>(
+ this: This,
+ term: string,
+ dataset: DatasetCore,
+ factory: DataFactory,
+ ): InstanceType & NamedNode
+
+ /**
+ * Creates a new instance of this class (or subclass), typed as both the wrapper and the type of the {@link Term} it wraps.
+ *
+ * @remarks
+ * This factory is equivalent to invoking the constructor directly, differing only in the return type: the constructor returns the declared instance type, whereas this factory returns the intersection of the (sub)class instance type and the type of the `term` argument. Because the result _is_ a {@link Term} at the type level, it can be passed directly anywhere an RDF/JS term is expected — for example when creating quads with a {@link DataFactory} or when matching with {@link DatasetCore.match} — without casts.
+ *
+ * @example Wrapping a literal
+ * The term type of the argument is preserved in the return type, so term-type-specific members are available without casts:
+ * ```ts
+ * const literal = factory.literal("some value", "en")
+ * const instance = SomeClass.from(literal, dataset, factory)
+ * // instance is typed as SomeClass & Literal
+ *
+ * const language = instance.language // contains "en"
+ * ```
+ *
+ * @example Using created instances directly as RDF/JS terms
+ * In contrast to instances created with the constructor, no casts are required to use the result as a term:
+ * ```ts
+ * const instance = SomeClass.from(factory.blankNode(), dataset, factory)
+ *
+ * // Used as subject when creating a quad
+ * factory.quad(instance, predicate, object)
+ *
+ * // Used as subject when matching statements in a dataset
+ * dataset.match(instance)
+ * ```
+ *
+ * @param term The original term being wrapped.
+ * @param dataset The dataset that contains the term being wrapped.
+ * @param factory A collection of methods for creating terms.
+ * @returns A new instance of the class this method was invoked on, typed additionally as the {@link Term} it wraps.
+ *
+ * @see
+ * - [Terms in the RDF/JS Data model specification](https://rdf.js.org/data-model-spec/#term-interface)
+ */
+ public static from any>(
+ this: This,
+ term: T,
+ dataset: DatasetCore,
+ factory: DataFactory,
+ ): InstanceType & T
+
+ public static from(this: any, term: string | Term, dataset: DatasetCore, factory: DataFactory): any {
+ return new this(term, dataset, factory)
+ }
+
+ //#endregion
+
/**
* The dataset that contains this term.
*
diff --git a/test/unit/term_wrapper_from.test.ts b/test/unit/term_wrapper_from.test.ts
new file mode 100644
index 0000000..fe55eb3
--- /dev/null
+++ b/test/unit/term_wrapper_from.test.ts
@@ -0,0 +1,98 @@
+import assert from "node:assert"
+import { describe, it } from "node:test"
+import { DataFactory } from "n3"
+import { TermWrapper } from "@rdfjs/wrapper"
+import { Child } from "./model/Child.js"
+import { datasetFromRdf } from "./util/datasetFromRdf.js"
+import { Example } from "./vocabulary/Example.js"
+import type { NamedNode, Quad_Subject, Term } from "@rdfjs/types"
+
+const rdf = `
+prefix :
+
+ :hasString "string 1" .
+`
+
+await describe("Term Wrapper from", async () => {
+ const dataset = datasetFromRdf(rdf)
+
+ await describe("Runtime behaviour", async () => {
+ await it("creates an instance of the subclass it is invoked on", async () => {
+ const child = Child.from("x", dataset, DataFactory)
+
+ assert.equal(child instanceof Child, true)
+ assert.equal(child instanceof TermWrapper, true)
+ })
+
+ await it("creates an instance of the base class when invoked on it", async () => {
+ const wrapper = TermWrapper.from("x", dataset, DataFactory)
+
+ assert.equal(wrapper instanceof TermWrapper, true)
+ })
+
+ await it("wraps a string as a named node, like the constructor", async () => {
+ const child = Child.from("x", dataset, DataFactory)
+ const constructed = new Child("x", dataset, DataFactory)
+
+ assert.equal(child.termType, "NamedNode")
+ assert.equal(child.value, "x")
+ assert.equal(child.equals(constructed as Term), true)
+ assert.equal(child.equals(DataFactory.namedNode("x")), true)
+ })
+
+ await it("wraps a given term instance, like the constructor", async () => {
+ const child = Child.from(DataFactory.blankNode("b1"), dataset, DataFactory)
+
+ assert.equal(child.termType, "BlankNode")
+ assert.equal(child.value, "b1")
+ })
+
+ await it("exposes accessors of the subclass", async () => {
+ const child = Child.from("x", dataset, DataFactory)
+
+ assert.equal(child.hasString, "string 1")
+ })
+
+ await it("keeps references to the dataset and factory", async () => {
+ const child = Child.from("x", dataset, DataFactory)
+
+ assert.equal(child.dataset, dataset)
+ assert.equal(child.factory, DataFactory)
+ })
+ })
+
+ await describe("Intersection return type", async () => {
+ await it("is assignable to term types without casts", async () => {
+ const child = Child.from("x", dataset, DataFactory)
+
+ // Compile-time assertions: no casts in any of the assignments below
+ const node: NamedNode = child
+ const subject: Quad_Subject = child
+
+ assert.equal(node.equals(subject), true)
+ })
+
+ await it("can be used to create quads without casts", async () => {
+ const child = Child.from("x", dataset, DataFactory)
+ const quad = DataFactory.quad(child, DataFactory.namedNode(Example.hasString), DataFactory.literal("string 2"))
+
+ assert.equal(quad.subject.equals(child), true)
+ })
+
+ await it("can be used to match quads in a dataset without casts", async () => {
+ const child = Child.from("x", dataset, DataFactory)
+ const matches = dataset.match(child)
+
+ assert.equal(matches.size, 1)
+ })
+
+ await it("preserves the term type of the wrapped term", async () => {
+ const literal = DataFactory.literal("some value", "en")
+ const wrapped = Child.from(literal, dataset, DataFactory)
+
+ // Compile-time assertion: literal members are available without casts
+ assert.equal(wrapped.language, "en")
+ assert.equal(wrapped.datatype.value, "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString")
+ })
+ })
+})