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:
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.
// ✔ 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.