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()
}
}
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'))
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].
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:
-
p
- aNode
with a label ofPerson
with properties includingname
andborn
-
r
- aRelationship
of typeACTED_IN
with properties includingroles
- an array of strings -
m
aNode
with a label ofMovie
.
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'
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>
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;
}>
Relationships almost almost identical, but use the Relationship
type instead.
type ActedIn = Relationship<Integer, {
roles: string[];
}>
These types can be combined within an interface to represent the each record in the result:
interface PersonActedInMovie {
p: Person;
r: ActedIn;
m: Movie;
}
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' }))
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.
Suggesting Properties
TypeScript is aware that people
is an array of Person
nodes, and properties defined in the interface can be suggested while typing.
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'
)
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[]'.
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)