NapNodes

← Discord bot guides

Discord bots · updated 25 October 2026

Discord Bot in TypeScript (discord.js): From Zero to Always Online

TypeScript is JavaScript with types: it warns you before the bot starts that “sides” may be empty or that you mistyped a name, instead of you finding out when the bot has already crashed on your server. discord.js ships with its own types. This bot has /hello and a /dice with an option, and at the end there's how to keep it online without your computer.

1. Create the bot on Discord

  1. Create the application

    Go to discord.com/developers/applications, click “New Application” and give it a name. That's your bot.

  2. Get the token

    In the “Bot” tab, click “Reset Token” and copy the token. It's the bot's password: whoever has it controls the bot, so don't share it or push it to GitHub.

  3. Invite it to your server

    In “OAuth2” → “URL Generator”, tick “bot” and “applications.commands”, pick the permissions (to start with, “Send Messages”) and open the link it gives you. To see what each permission lets the bot do, build the link with the bot permissions calculator.

Rather not set it up yourself? Discord bot hosting from €1.50 a month, no contract and a full refund within 72 hours. Or create a free Discord bot, no card needed.

2. The project: package.json and tsconfig.json

With Node.js 20 or newer, create a folder with these two files and a src subfolder. dependencies lists what the bot uses while running (discord.js and dotenv); devDependencies, what you only need to write it: TypeScript (the compiler, tsc), the Node.js types and tsx, which runs the .ts directly while you test it. Then run “npm install”.

tsconfig.json says the code lives in src and the compiled JavaScript goes to dist. “strict” turns on every check (that's what makes TypeScript worth it) and “skipLibCheck” skips type-checking the libraries, which is the slowest part.

package.json
{
  "name": "my-bot",
  "version": "1.0.0",
  "private": true,
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "tsx watch src/index.ts"
  },
  "dependencies": {
    "discord.js": "^14.16.0",
    "dotenv": "^16.4.0"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "tsx": "^4.19.0",
    "typescript": "^5.6.0"
  }
}
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "src",
    "outDir": "dist",
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}
.env
DISCORD_TOKEN=paste-your-token-here

3. The code, with types

It's the same bot as the JavaScript one, with two differences you'll notice: if the token is missing, it stops with a clear message instead of a cryptic Discord error; and getInteger returns number | null, so TypeScript won't let you use “sides” without saying what happens when nobody types it (the “?? 6”).

The ChatInputCommandInteraction in the dice function is the type of a slash command: your editor shows you everything it has (options, user, reply…) as you type.

src/index.ts
import "dotenv/config"; // reads the token from the .env file
import { Client, Events, GatewayIntentBits, SlashCommandBuilder, type ChatInputCommandInteraction } from "discord.js";

const token = process.env.DISCORD_TOKEN;
if (!token) throw new Error("DISCORD_TOKEN is missing from the .env file");

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

const commands = [
  new SlashCommandBuilder().setName("hello").setDescription("Says hello"),
  new SlashCommandBuilder()
    .setName("dice")
    .setDescription("Rolls a die")
    .addIntegerOption((o) => o.setName("sides").setDescription("How many sides (6 if you don't say)").setMinValue(2).setMaxValue(100)),
];

async function dice(i: ChatInputCommandInteraction): Promise<void> {
  const sides = i.options.getInteger("sides") ?? 6; // number | null: TypeScript makes you handle the “didn't say” case
  await i.reply(`🎲 You rolled a ${1 + Math.floor(Math.random() * sides)} (${sides}-sided die)`);
}

client.once(Events.ClientReady, async (c) => {
  await c.application.commands.set(commands.map((x) => x.toJSON())); // publishes /hello and /dice
  console.log(`Logged in as ${c.user.tag}`);
});

client.on(Events.InteractionCreate, async (i) => {
  if (!i.isChatInputCommand()) return;
  if (i.commandName === "hello") await i.reply(`Hello, ${i.user}!`);
  if (i.commandName === "dice") await dice(i);
});

client.login(token);

4. Test it and compile it

To test it while you write it: “npm run dev”. tsx starts it without compiling and restarts it every time you save. When it says “Logged in as…”, type /hello or /dice on your server (slash commands can take a few minutes to show up the first time; if they don't, what to check).

To get it ready: “npm run build”. tsc checks the types (if something doesn't fit, it tells you the file and line, and doesn't compile) and writes the JavaScript to dist/index.js. That's what actually runs, with “npm start”.

5. Keep it online all the time (for free)

As long as the program is running on your computer, the bot works; turn the computer off and it disconnects. To keep it online all the time it has to be hosted on a server, and there the reliable way is to run the compiled JavaScript, not the .ts.

Why compile on your computer: tsc uses memory. Compiling this very bot, in our test tsc reached about 250 MB, almost everything the free bot has (256 MB). Compiling on every start, with tsc or with ts-node, leaves it at the limit and makes it slower to start; the compiled bot uses much less.

  1. Compile

    “npm run build” on your computer. You end up with dist/index.js.

  2. Create the bot and upload the files

    Create the free bot on NapNodes (it starts out on Node.js, so there's no language to change). In “Files”, upload package.json and .env; with “Create Directory” make dist and upload index.js inside it. Don't upload the node_modules folder: the libraries in package.json install themselves at startup.

  3. Change the startup file

    In “Settings”, under “Startup file”, put dist/index.js instead of index.js. That way it runs with plain Node.js: if you put a file ending in .ts, the startup tries to run it with ts-node, which is exactly what we want to avoid.

  4. Start

    Click “Start”. The “Console” shows what your bot prints: you should see “Logged in as…”. If it connects but doesn't reply, what to check in each case.

If your code is on GitHub

Put the repository address in “Settings” → “GitHub repository” and it downloads itself; with “Pull repository changes on start”, every restart brings the latest. The .env doesn't go in the repository (add it to .gitignore): upload it separately in “Files”.

Since the server doesn't compile, the repository has to include dist already compiled: remove dist from .gitignore and run “npm run build” before every commit. If you'd rather compile on the server, add "prepare": "tsc" to the scripts in package.json (npm runs it after installing the libraries, on every start), but do it on a plan with more memory than the free one, because of what tsc uses.

How much memory it needs

A command bot like this one, once compiled, runs comfortably on the 256 MB free bot, which you only have to renew with one click every week. If it grows (many servers, big cache, music) or you'd rather not have to remember, the Mini plan has 512 MB and the Plus 1 GB, always online with nothing to renew (Discord bot pricing); the RAM calculator tells you which.

Frequently asked questions

Can you make a Discord bot in TypeScript?

Yes. discord.js is written with TypeScript in mind and ships its types, so all you need is to install typescript and @types/node, a tsconfig.json and compile with tsc. What runs is the compiled JavaScript.

ts-node or compile with tsc?

While coding, tsx or ts-node are handy because they run the .ts without compiling. For hosting, compile with tsc and run the JavaScript: it starts faster and uses less memory, because it doesn't compile on every start.

Why won't my TypeScript bot start on the host?

Almost always because the host is trying to run the .ts or a dist that doesn't exist. Compile with “npm run build”, upload dist/index.js and set that file as the startup file. If the console says “DISCORD_TOKEN is missing”, the .env is missing.

What's the difference between dependencies and devDependencies?

dependencies is what the bot needs to run (discord.js, dotenv); devDependencies, what you only use to write it (typescript, tsx, the types). The compiled bot doesn't need TypeScript to run.

Shall we set it up for you?

Discord bot hosting from €1.50 a month, never oversold: your RAM is yours alone. And if you'd rather start without paying, there's a free Discord bot, no card needed.

Keep reading

All Discord bot guides, sorted →