Skip to content

feat: make TermWrapper generic over the wrapped term (TermWrapper<T extends Term = Term>)#85

Draft
jeswr wants to merge 1 commit into
rdfjs:mainfrom
jeswr:feat/term-wrapper-generic
Draft

feat: make TermWrapper generic over the wrapped term (TermWrapper<T extends Term = Term>)#85
jeswr wants to merge 1 commit into
rdfjs:mainfrom
jeswr:feat/term-wrapper-generic

Conversation

@jeswr

@jeswr jeswr commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Makes TermWrapper generic over the term it wraps:

export class TermWrapper<T extends Term = Term> implements IRdfJsTerm

What it does

  • The Term constructor overload now takes T and original is typed T, so the wrapped term type is inferred at the construction site: new TermWrapper(factory.literal("some value"), dataset, factory) is a TermWrapper<Literal>.

  • termType returns T["termType"], so it narrows to the literal type of the wrapped term ("Literal", "NamedNode", …). The compiler catches impossible comparisons (literalWrapper.termType === "NamedNode" is a build error) and unions of wrappers discriminate on it:

    let wrapper: TermWrapper<NamedNode> | TermWrapper<Literal>
    
    if (wrapper.termType === "Literal") {
        wrapper // narrowed to TermWrapper<Literal>
    }
  • The parameter defaults to Term, so the change is non-breaking: bare TermWrapper references, class SomeClass extends TermWrapper, ITermWrapperConstructor and all mapping helpers compile unchanged, and TermWrapper<Literal> remains assignable to TermWrapper (covariant). The string overload of the constructor is untouched (the IRI is wrapped as a NamedNode; T stays at the default there).

Why

This is the class-generic slice of the typing work in #61/#74, isolated so it can be discussed on its own merits. The rationale is the one @jeswr gave in #61 (comment): errors at build time instead of run time, less as for consumers, and avoiding false types — with the editor able to narrow what a wrapper can be based on the term it wraps. Because this slice keeps the default parameter and does not touch the getters, consumers who never name the parameter see exactly the API they see today.

What it deliberately does not do

Relation to existing PRs

Supersedes the generic-parameter slice of #61 and of #74 (the class TermWrapper<T extends Term> declaration itself). The remaining slices of those PRs (from/in factories → #83, getter/IRdfJsTerm restructuring, TermNode mapper signatures) are intentionally not included.

Testing

  • New test/unit/term_wrapper_generic.test.ts: runtime assertions plus compile-level assertions — inference from the constructor, discriminant narrowing of wrapper unions, covariant assignability to the default TermWrapper, subclasses with an explicit type argument working with the mapping helpers, @ts-expect-error proving the impossible-comparison diagnostic, and the term-type-specific getters remaining available.
  • npm test: 131 tests, 0 fail (5 pre-existing skips); the new constructor/getter paths are covered.
  • npx typedoc: 0 errors, 6 warnings — identical to main.

Review timing: This draft was prepared with Claude; I (@jeswr) will personally review it before it progresses. I'm currently batching a lot of work in flight, so expect active review Wednesday-Friday (8-10 July).

Adds a class-level type parameter with a default, TermWrapper<T extends
Term = Term>, so the type of the wrapped term is inferred from the
constructor argument and termType narrows to T["termType"]. The default
keeps every existing declaration and call site compiling unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant