DEV Community

Adam Cowley
Adam Cowley

Posted on • Updated on

Using TypeScript with Neo4j

TL;DR: You can learn more in the Building Neo4j Applications with TypeScript course
on Neo4j GraphAcademy

The recent 5.2.0 release of the Neo4j JavaScript Driver features some significant improvements for TypeScript users. So much so that it has inspired me to write an article.

It is now possible to use an interface to define the type of records returned by your Cypher query, giving you the added benefit of type-checking and type hinting while processing results.

A Worked Example

For example, let's take a query from the Recommendations Dataset. Say we would like to find a list of all actors that have appeared in a movie.

To find this, we would need to create a new driver instance, open up a new session and then use the executeRead() function to send a Cypher statement and await the result.

async function main() {
  // Create a Driver Instance
  const driver = neo4j.driver(
    'neo4j://localhost:7687',
    neo4j.auth.basic('neo4j', 'letmein!')
  )

  // Open a new Session
  const session = driver.session()

  try {
    // Execute a Cypher statement in a Read Transaction
    const res = await session.executeRead(tx => tx.run(`
      MATCH (p:Person)-[r:ACTED_IN]->(m:Movie {title: $title})
      RETURN p, r, m
    `, { title: 'Pulp Fiction' }))

    const people = res.records.map(row => row.get('p'))

    console.log(people)
  }
  finally {
    // Close the Session
    await session.close()
  }
}
Enter fullscreen mode Exit fullscreen mode

Straightforward enough, and as are all perfect developers we will never experience any problems writing code this way.

On the other hand, if someone happens to make a typo in the line res.records.map(row => row.get('p')) or tries to .get() a value that isn't returned in the result, the Driver is written to throw an Error.

Say that row is changed to:

const people = res.records.map(row => row.get('something'))
Enter fullscreen mode Exit fullscreen mode

As something doesn't exist in the result, a Neo4jError will be thrown:

Neo4jError: This record has no field with key 'something', 
available key are: [p,r,m].
Enter fullscreen mode Exit fullscreen mode

You will eventually find this out when you run the application, but the whole point of TypeScript is to identify these errors during the development process.

Adding Type-Checking

To protect against this type of scenario, we can now use an interface to define the keys available on each record.

In the case of the query above, we have three values:

  1. p - a Node with a label of Person with properties including name and born
  2. r - a Relationship of type ACTED_IN with properties including roles - an array of strings
  3. m a Node with a label of Movie.

The neo4j-driver library exports two type definitions, Node and Relationship, that we can use to define these items.

import neo4j, { Node, Relationship } from 'neo4j-driver'
Enter fullscreen mode Exit fullscreen mode

Both of these classes accept generics to define the type of the .identity and the properties held on the value.

Unless you have set the disableLosslessIntegers option when creating the Driver, the identity will be an instance of the Integer type exported from neo4j-driver.

Person values can be defined as a TypeScript type.

import { Integer } from 'neo4j-driver'

interface PersonProperties {
  tmdbId: string;
  name: string;
  born: number; // Year of birth
}

type Person = Node<Integer, PersonProperties>
Enter fullscreen mode Exit fullscreen mode

Or, for a more terse example, you can define the properties directly in the second generic:

type Movie = Node<Integer, {
  tmdbId: string;
  title: string;
  rating: number;
}>
Enter fullscreen mode Exit fullscreen mode

Relationships almost almost identical, but use the Relationship type instead.

type ActedIn = Relationship<Integer, {
  roles: string[];
}>
Enter fullscreen mode Exit fullscreen mode

These types can be combined within an interface to represent the each record in the result:

interface PersonActedInMovie {
  p: Person;
  r: ActedIn;
  m: Movie;
}
Enter fullscreen mode Exit fullscreen mode

Both the session.run() and tx.run() accept the interface and add type checking to any subsequent processing. The above example can be updated to pass the PersonActedInMovie interface to the tx.run() method call.

// Execute a Cypher statement in a Read Transaction
const res = await session.executeRead(tx => tx.run<PersonActedInMovie>(`
  MATCH (p:Person)-[r:ACTED_IN]->(m:Movie {title: $title})
  RETURN p, r, m
`, { title: 'Pulp Fiction' }))
Enter fullscreen mode Exit fullscreen mode

Type Checking in Action

As the record shape has been defined, TypeScript will now validate the code as it is written and provide suggestions.

Suggesting Record Keys

Suggestions are now provided when calling the record.get() method.

VS Code suggests p, r and m as possible values

Suggesting Properties

TypeScript is aware that people is an array of Person nodes, and properties defined in the interface can be suggested while typing.

VS Code suggests potential properties for the Person node

Checking Property Keys

If a key does not exist in the properties of a node or relationship, TypeScript will pick this up straight away and throw an error:

const names = people.map(
  person => person.properties.foo 
  // Property 'foo' does not exist
  // on type 'PersonProperties' 

)
Enter fullscreen mode Exit fullscreen mode

Type-checking Properties

TypeScript is now also be aware of the type of each of the properties, so TypeScript will throw an error if you try to use a value that is not defined in the Type.

const names: string[] = people.map(
  person => person.properties.born
)
// Type 'number[]' is not assignable to type 'string[]'.
Enter fullscreen mode Exit fullscreen mode

Interested in learning more?

If you are interesting in learning more about Neo4j, I recommend checkout out the Beginners Neo4j Courses on GraphAcademy.

If you are interested in learning more, I am currently working on a new Neo4j & TypeScript course for Neo4j GraphAcademy.

You can also learn everything you need to know about using Neo4j in a Node.js project in the Building Neo4j Applications with Node.js
course, in which you will take a deeper dive into the Driver lifecycle and replace hardcoded data with responses from a Neo4j Sandbox instance.

The Building Neo4j Applications with TypeScript course
is a shorter, two hour course that covers the fundamentals of the Neo4j JavaScript Driver along with additional TypeScript features.

If you have any comments or questions, feel free to reach out to me on Twitter.

Top comments (0)