Use TypeScript

TypeScript provides highly productive development tools for JavaScript IDEs and practices, like static checking. It makes code easier to read and understand, there is a lot to gain using TypeScript over plain JavaScript.

Before diving deeper into this guide, please take some time to read the previous guides as we will only cover TypeScript-related things in this one :)

📘

Source code

You can find the resulting repository of this guide on our Github.

Setting up our project

Phantombuster's SDK

We made an SDK to ease the development process. You should install it globally. It will be really useful to upload the source code every time a change is made in the source code.

npm install -g phantombuster-sdk

You now need to fill your Phantombuster config file (phantombuster.cson), we will come back to this file later., for now it should look like so:

[
	name: 'My first TypeScript Phantom'
	apiKey: 'YOUR_API_KEY'
	scripts:
]

Some useful resources:

Dependencies

We are now missing TypeScript compiler and the types we will be using in our project:

npm install --save-dev typescript  @types/[email protected] @types/node

🚧

Puppeteer

Puppeteer types version should match the version provided in the Phantombuster package you are using. We wrote a list of all available packages for you to know which modules are included in the package.

We will also need types from our BusterJS agent module, they are available in our
public gists.

Lets setup TypeScript with strict tsconfig.json file, to take the most out of TypeScript:

{
	"compilerOptions": {
		"moduleResolution": "node",
		"target": "es5",
		"module": "esNext",
		"outDir": "dist",
		"lib": ["dom", "es2015"],
		"esModuleInterop": true,
		"strict": true,
		"noImplicitAny": true,
		"strictNullChecks": true,
		"strictFunctionTypes": true,
		"alwaysStrict": true,
	},
	"include": ["./src"]
}

Make our script run on Phantombuster

We are ready to code our TypeScript script, let's create a new index.ts file in the src folder:

mkdir src
touch src/index.ts

For now lets write a really simple script, just to see it run, under src/index.ts:

"phantombuster command: nodejs"
"phantombuster package: 5"

;(async () => {
	console.log("Hello from the ether side!")
	process.exit()
})()

Phantombuster can not execute TypeScript, so you need to compile it into JavaScript using tsc. It will read our tsconfig.json where we included our src folder, and transpile the TypeScript files inside to the dist folder (outDir), keeping the exact same structure.

tsc

We now have a dist/index.js file that corresponds to our transpiled TypeScript, let's upload it to Phantombuster using the SDK. First, let's create a mapping of Phantombuster script names to local script files. Our local file dist/index.js will be mapped to hackerNews.js on Phantombuster.

[
    name: 'account name'
    apiKey: 'YOUR_API_KEY_HERE'
    scripts:
        'hackerNews.js': 'dist/index.js'
]

Let's upload it!

phantombuster dist/index.js

You can now go to your catalog, click "Use this Phantom" (this is a user friendly way of saying "Create an agent from this script).

Go through the setup, you are on your agent's console. You should open your console to debug your script: open your management menu and click Toggle console. You can now launch it and see it run live!

Yay! Your script has been executed!

Yay! Your script has been executed!

Automate the process

This is such a heavy process of upload, and we don't want to keep doing it every time we update something.

The new process using watcher modes is as follows:

  • Open a terminal and run TypeScript in watcher mode. It will proceed to an incremental compilation each time our TypeScript code is updated.
tsc --watch
  • The generated JavaScript file is now automatically synchronized with our source TypeScript. It's time to synchronize it with Phantombuster. Open a new terminal and launch the Phantombuster SDK in watch mode, it will monitor a directory on your disk for changes in your scripts. As soon as a change is detected, the script will be uploaded to your Phantombuster account. SDK: automatically publish code to Phantombuster →
phantombuster

That's it, every change you make in your source TypeScript file will update your Phantom's code!

Write a complete script

In this section, we will rewrite our script from the developer's quick start using TypeScript! We will scrape HackerNews and extract some precious data.

First let's copy the JavaScript source into our srcx/index.ts file:

"phantombuster package: 5"
"phantombuster command: nodejs"
"phantombuster flags: save-folder"

const Buster = require("phantombuster")
const puppeteer = require("puppeteer")

const buster = new Buster()

;(async () => {
	const browser = await puppeteer.launch({
		// This is needed to run Puppeteer in a Phantombuster container
		args: ["--no-sandbox"]
	})
	
	const page = await browser.newPage()
	await page.goto("https://news.ycombinator.com")
	await page.waitForSelector("#hnmain")

	const hackerNewsLinks = await page.evaluate(() => {
		const data = []
		document.querySelectorAll("a.storylink").forEach((element) => {
			data.push({
				title: element.text,
				url: element.getAttribute("href")
			})
		})
		return data
	})

	await buster.setResultObject(hackerNewsLinks)
	await page.screenshot({ path: "hacker-news.png" })

	await page.close()
	await browser.close()
	process.exit()
})()

We copied some JavaScript into a TypeScript file, we immediately got some errors, let's fix them. I will begin with this one:

Line 21: Variable 'data' implicitly has type 'any[]' in some locations where its type cannot be determined.

data has an implicit Array<any> type which makes TypeScript's static checking useless. This error is raised because we set "noImplicitAny" to true in our tsconfig.json.

What information does an item of data contains? A title and an url, both should be strings.

In TypeScript, this type declaration can be made through an interface:

[...]
interface IHackerNewsLink {
	title: string
	url: string
}
[...]

We can now declare our data variable as being an array of IHackerNewsLink:

[...]
const data: IHackerNewsLink[] = []
[...]

TypeScript seems satisfied with our update on this error, but another one has been raised:

Type 'string | null' is not assignable to type 'string'. Type 'null' is not assignable to type 'string'. The expected type comes from property 'url' which is declared here on type 'IHackerNewsLink'.

This error is caused by getAttribute("href") that returns string | null, making the url property of our result string | null too, which is not assignable to string.

We want to make sure that we can grab a string value for our url property.

document.querySelectorAll returns a list typed as NodeListOf<Element>. Element is not as precise as we would like it to be, we know that we are querying HTMLAnchorElement elements (see the selector's a.storylink), and it always has a string property called href that looks exactly like what we are looking for!

There are 3 ways around this problem:

  • an ugly cast that annihilates the usefulness of TypeScript, let's not do this.
document.querySelectorAll("a.storylink").forEach((element) => {
	data.push({
		title: (element as HTMLAnchorElement).text,
		url: (element as HTMLAnchorElement).href
	})
})
  • use document.querySelectorAll's generic type as follow:: document.querySelectorAll<HTMLAnchorElement>("a.storylink"), but it is like a cast, nothing is strongly checked and your type and query could be completely uncorrelated. Don't do this either.
document.querySelectorAll<HTMLAnchorElement>("a.storylink").forEach((element) => {
	data.push({
		title: element.text,
		url: element.href
	})
})
  • create a logical type guard statement checking that element trully is an HTMLAnchorElement, no runtime error on this one!
document.querySelectorAll("a.storylink").forEach((element) => {
	if (element instanceof HTMLAnchorElement) {
		data.push({
			title: element.text,
			url: element.href,
		})
	}
})

Have you noticed that this change also fixed the last error we had? This one:

Line 25: Property 'text' does not exist on type 'Element'

Property text might not exist on type Element, but it surely does on type HTMLAnchorElement!

Conclusion

Your precious data is now available on your agent's console, scroll a bit and click on the last run under the "Recent activity" section.

Your precious data is now available on your agent's console, scroll a bit and click on the last run under the "Recent activity" section.

📘

Congratulations!

You just wrote your first script using TypeScript! The whole source code of this guide is available on our Github.