How to add your own type definitions to DefinitelyTyped

Script!!!

Recently I started using TypeScript (TS) with React Native. Now I won’t be going over the benefits of typescript in this article there are plenty for other articles that will explain the benefits (and drawbacks).

Image for post
Image for post
TypeScript!

TS is a superset of JavaScript (JS) so anything JS can do TS can do (and more). One of the main advantages of TS is it’s strict type checking. JS is weakly typed which means variable and parameters can be of any type. One of the major downsides of this approach is in larger projects it can make code harder to follow and more bug prune. For example, if you’re expecting a variable to be an integer but turns out to be a string. Typescript makes bugs like this much easier to catch because it is strongly typed and each variable and parameter is given a type. Lets say you have the following function.

add = (x, y) => {
return x + y;
};

Now we expect and to be integers here of course however we are not checking types so let's say we did the following.

add(2, 3) === 5; // true
add(2, "3") === "23"; // true
add("2", "3") === "23"; // true

As you can see if you accidentally passed a string to it returns a result we don't expect. TS helps us catch theses types of errors. The following would be the equivalent functions written in TS.

add = (x: number, y: number) => {
return x + y;
};

Definitely Typed

When using JS libraries not written in TS we need a file which stores the type definitions of functions and their parameters this is referred to as the global type definition file. Lots of popular libraries already have this defined in a huge project on GitHub called . You can actually add these to your project by using .

This repo is huge and has over 5,000 libraries already defined however for more obscure projects you may have to write you’re own definitions. This then means we can take full advantage of TS even with any external libraries we use. In this article, we will write definitions for .

  1. Fork the project on GitHub, how to fork on GitHub.
  2. Git clone the project onto your computer, like so .
  3. Open the project in your favourite text editor and run the following commands in the root (project) directory.
  4. Execute the following command using either or , replace with your package name. Before you run the command you should make sure the package doesn't exist in which case all you likely need to do is update its type definitions
  5. You should see a new folder with the package name in the folder.
yarn
yarn npx dts-gen --dt --name react-native-canvas --template module

# or

npm install
npm npx dts-gen --dt --name react-native-canvas --template module

tsconfig.json

You should now have four auto-generated files, we can leave as it is. Since this a React Native library we will have to edit with some new parameters. If you're confused you can take a look at other type packages to see how they've changed the file. There are plenty of React Native examples to take a look at. The now looks like this

{
"compilerOptions": {
"module": "commonjs",
"lib": ["es6"],
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"baseUrl": "../",
"typeRoots": ["../"],
"types": [],
"noEmit": true,
"forceConsistentCasingInFileNames": true,
"jsx": "react-native"
},
"files": ["index.d.ts", "react-native-canvas-tests.tsx"]
}

index.d.ts

Now onto the main file to edit index this contains the types for the library. So now we will have to look at the library itself and take a look at the functions components etc. If the file has been created properly at the top in comments you should see something like this.

// Type definitions for react-native-canvas 0.1
// Project: https://github.com/iddan/react-native-canvas#readme
// Definitions by: hmajid2301 <https://github.com/hmajid2301>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 3.1

The first two lines are auto-generated, the next line I added my name and the URL to my GitHub account. The following line is also auto-generated and the final line is required because we are defining our types with .

Now we actually need to look at the library, so we know how to define our types correctly. The source code is in the folder , now the first class I use is . Here is a small snippet of the source code.

...
export default class Canvas extends Component {
static propTypes = {
style: PropTypes.shape(ViewStylePropTypes),
baseUrl: PropTypes.string,
originWhitelist: PropTypes.arrayOf(PropTypes.string),
};
...
}

The main thing I am interested in is the we will need to define these in the file. So here we have a React Native component class , in the file this will become in this class, we don't have any state if we did then it would look like .

Now we’ve defined our class lets define our props we will define our props as an interface called which will be defined like so.

export interface CanvasProps {
style?: StyleProp<ViewStyle>;
baseUrl?: string;
originWhitelist?: string[];
ref: (canvas: Canvas) => any;
}

The first objects are the same as the first three prop types in the original JS library. They are defined almost exactly the same bar some syntax differences, in JS as a pose to in TS. However in the original, the prop is not defined, so we define it ourselves for completeness, . In this case, the prop takes an input of type and can return anything. Below is an example of being used (in JS).

class App extends Component {
handleCanvas = canvas => {
const ctx = canvas.getContext("2d");
ctx.fillStyle = "purple";
ctx.fillRect(0, 0, 100, 100);
};

render() {
return <Canvas ref={this.handleCanvas} />;
}
}

In our class, we have to define our properties, according to the documentation we have the following functions/attributes.

  • Canvas#height
  • Canvas#width
  • Canvas#getContext()
  • Canvas#toDataURL()

These get defined as follows;

width: number;
height: number;
toDataURL: () => string;
getContext: (context: string) => CanvasRenderingContext2D;

This should all be pretty straight forward, the final property returns . This another interface we define using the class (separate file in folder). It's quite a long interface so if you want to see it here.

We then repeat this process for the remaining classes, , which look like follows. In these classes, we also define the constructor, which just contains the arguments and the type of object they expect. Note that these classes aren't React Native components so we define them as normal classes. We also give them named exports i.e. rather than , this is because this is how they are defined in the library.

export class Image {
constructor(canvas: Canvas, height?: number, width?: number);
crossOrigin: string | undefined;
height: number | undefined;
width: number | undefined;
src: string | undefined;
addEventListener: (event: string, func: (...args: any) => any) => void;
}

export class ImageData {
constructor(canvas: Canvas, data: number[], height: number, width: number);
readonly data: number[];
readonly height: number;
readonly width: number;
}

The final class to define is , which looks like

export class Path2D {
constructor(canvas: Canvas, ...args: any);
addPath: (
path: Path2D,
transform?: {
a: number;
b: number;
c: number;
d: number;
e: number;
f: number;
}
) => void;

closePath: CanvasRenderingContext2D["closePath"];
moveTo: CanvasRenderingContext2D["moveTo"];
lineTo: CanvasRenderingContext2D["lineTo"];
bezierCurveTo: CanvasRenderingContext2D["bezierCurveTo"];
quadraticCurveTo: CanvasRenderingContext2D["quadraticCurveTo"];
arc: CanvasRenderingContext2D["arc"];
arcTo: CanvasRenderingContext2D["arcTo"];
ellipse: CanvasRenderingContext2D["ellipse"];
rect: CanvasRenderingContext2D["rect"];
}

Again this class is very similar to the classes defined above except some of the properties look like . This is because shares the same definition as closePath in , which is defined as . So rather than define it twice we just copy the definition in .

react-native-canvas-tests.jsx

This is where we define some tests how the library should be used and their props types.

import * as React from "react";
import { View } from "react-native";
import Canvas, {
Image as CanvasImage,
Path2D,
ImageData
} from "react-native-canvas";

class CanvasTest extends React.Component {
render() {
return (
<View>
<Canvas ref={this.handleCanvas} />
</View>
);
}
...

So we import our library then we render our component.

handleCanvas = (canvas: Canvas) => {
canvas.width = 100;
canvas.height = 100;

const context = canvas.getContext("2d");
context.fillStyle = "purple";
context.fillRect(0, 0, 100, 100);

const ellipse = new Path2D(canvas);
ellipse.ellipse(50, 50, 25, 35, (45 * Math.PI) / 180, 0, 2 * Math.PI);
context.fillStyle = "purple";
context.fill(ellipse);

const image = new CanvasImage(canvas);
canvas.width = 100;
canvas.height = 100;

image.src =
"https://upload.wikimedia.org/wikipedia/commons/6/63/Biho_Takashi._Bat_Before_the_Moon%2C_ca._1910.jpg";
image.addEventListener("load", () => {
context.drawImage(image, 0, 0, 100, 100);
});

const imageData = context.getImageData(0, 0, 100, 100);
const data = Object.values(imageData.data);
const length = Object.keys(data).length;
for (let i = 0; i < length; i += 4) {
data[i] = 0;
data[i + 1] = 0;
data[i + 2] = 0;
}
const imgData = new ImageData(canvas, data, 100, 100);
context.putImageData(imgData, 0, 0);
};

Then in , we test out the different classes we defined, include and that's it. The above example is taken from a few examples in within . Ok now we've defined our files lets make sure the pull request (PR) will be accepted let's run . If the linter doesn't complain then we can commit and push our changes to our GitHub fork and make PR.

Written by

Software Engineer | Pythonista | Typescripter | Docker Advocate | https://haseebmajid.dev

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store