documentationfor yFiles for HTML 3.0.0.1

TypeScript

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.

When you add the yFiles npm module to your project as recommended, no additional setup is necessary to enable TypeScript support. The yFiles npm module contains a typings file that will be automatically picked up by the TypeScript compiler and modern IDEs.

This section describes some best practices and tips for using yFiles for HTML with TypeScript.

Augmenting the tag property

The tag property allows you to store user-defined data for items such as INode and IEdge. By default, these tag properties are typed as any. This is because the library doesn’t depend on these properties and can’t know in advance which types your application will use.

You can make your application more type-safe by using TypeScript’s module augmentation feature to restrict the type of tags:

Augmenting tags
import { type IGraph, type INode, ShapeNodeStyle } from '@yfiles/yfiles'

type MyNodeTag = {
  myNodeTagProp: string
}

type MyEdgeTag = {
  myEdgeTagProp: number
}

type MyLabelTag = {
  myLabelTagProp: boolean
}

declare module '@yfiles/yfiles' {
  interface INode {
    get tag(): MyNodeTag | null
    set tag(value: MyNodeTag)
  }

  interface IEdge {
    get tag(): MyEdgeTag | null
    set tag(value: MyEdgeTag)
  }

  interface ILabel {
    get tag(): MyLabelTag | null
    set tag(value: MyLabelTag)
  }
}

This allows the TypeScript compiler to be stricter when checking uses of tag properties and helps you avoid common mistakes.

Using augmented tags
// ✔ MyNodeTag has a "myNodeTagProp" property of type string
node.tag?.myNodeTagProp.toLowerCase()
// @ts-expect-error "myNodeTagProp" is not a number
node.tag?.myNodeTagProp.toFixed()
// @ts-expect-error MyNodeTag does not have a "helloWorld" property
node.tag?.helloWorld.toString()

node.tag = {
  // ✔
  myNodeTagProp: 'hello'
}
node.tag = {
  // @ts-expect-error typo
  myNodeTagFrop: 'hello'
}
node.tag = {
  // @ts-expect-error mixed up node and edge tags
  myEdgeTagProp: 5
}

Additionally, other parts of the API will automatically adapt to the user-provided types. For example, the tag parameter in createNode will expect the user-defined type for node tags instead of the default any type.

// ✔
graph.createNode([0, 0, 30, 30], new ShapeNodeStyle(), {
  myNodeTagProp: 'new node'
})
// @ts-expect-error invalid node tag, since `myNodeTagProp` is supposed to be a string
graph.createNode([0, 0, 30, 30], new ShapeNodeStyle(), {
  myNodeTagProp: Date.now()
})

Tagged and typed visuals

There are several utility types that make it easier to implement item styles in a type-safe and convenient manner when using TypeScript.

To illustrate, let’s look at a simplified structure of a custom node style:

type MyCache = {
  rx: number
  ry: number
}

class MyCustomNodeStyle extends NodeStyleBase<SvgVisual> {
  createVisual(context: IRenderContext, node: INode): SvgVisual | null {
    // create DOM element
    const ellipse = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'ellipse'
    )
    // cache relevant data
    const cache: MyCache = {
      rx: node.layout.width,
      ry: node.layout.height
    }
    // apply data to DOM element
    ellipse.rx.baseVal.value = cache.rx
    ellipse.ry.baseVal.value = cache.ry
    // attach cache to element so we can retrieve it in updateVisual
    ;(ellipse as any)['renderDataCache'] = cache
    return new SvgVisual(ellipse)
  }

  updateVisual(
    context: IRenderContext,
    oldVisual: SvgVisual,
    node: INode
  ): SvgVisual | null {
    // retrieve DOM element
    const ellipse = oldVisual.svgElement as SVGEllipseElement
    // retrieve cache from DOM element
    const cache: MyCache = (ellipse as any)['renderDataCache']
    // check if relevant data has changed
    if (cache.rx !== node.layout.width || cache.ry !== node.layout.height) {
      // if yes, apply changes to DOM
      cache.rx = node.layout.width
      cache.ry = node.layout.height
      ellipse.rx.baseVal.value = cache.rx
      ellipse.ry.baseVal.value = cache.ry
    }
    return oldVisual
  }
}

By using TaggedSvgVisual<TElement,TTag> together with SvgVisual.from<TElement,TTag>, we can eliminate the type assertions and make the code shorter and make the code more readable:

type MyCache = {
  rx: number
  ry: number
}
type MyEllipseVisual = TaggedSvgVisual<SVGEllipseElement, MyCache>

class MyCustomNodeStyle extends NodeStyleBase<MyEllipseVisual> {
  createVisual(context: IRenderContext, node: INode): MyEllipseVisual | null {
    // create DOM element
    const ellipse = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'ellipse'
    )
    // cache relevant data
    const cache: MyCache = {
      rx: node.layout.width,
      ry: node.layout.height
    }
    // apply data to DOM element
    ellipse.rx.baseVal.value = cache.rx
    ellipse.ry.baseVal.value = cache.ry
    return SvgVisual.from(ellipse, cache)
  }

  updateVisual(
    context: IRenderContext,
    oldVisual: MyEllipseVisual,
    node: INode
  ): MyEllipseVisual | null {
    // retrieve DOM element and cache - they already have the correct type
    const { svgElement: ellipse, tag: cache } = oldVisual
    // check if relevant data has changed
    if (cache.rx !== node.layout.width || cache.ry !== node.layout.height) {
      // if yes, apply changes to DOM
      cache.rx = node.layout.width
      cache.ry = node.layout.height
      ellipse.rx.baseVal.value = cache.rx
      ellipse.ry.baseVal.value = cache.ry
    }
    return oldVisual
  }
}

There are also equivalent helpers for HTML based visuals: TaggedHtmlVisual<TElement,TTag> and HtmlVisual.from<TElement,TTag>.

Similarly, if you do not need to pass data from createVisual to updateVisual, you can use the TypedSvgVisual<TElement> and SvgVisual.from<TElement> helpers (and their HTML equivalents) instead.