Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
TypeScript + Gatsby config and node API

README

  1. When Gatsby starts up, it will read gatsby-config.js first.
  2. As you can see below, we use that file to require('ts-node').register() which registers a TypeScript evaluator that will be used when Gatsby reads all other API Javascript files. In other words, we only need to do this once in our entire codebase and not in other Gatsby files like gatsby-node.js.
  3. Our gatsby-config.js re-exports all the exported variables available in gatsby-config.ts.
  4. Later, since the ts-node evaluator is still active, Gatsby will load gatsby-node.ts instead of gatsby-node.js.
  5. The same thing is true of other gatsby files; e.g. gatsby-browser.ts can be used instead of gatsby-browser.js.

Credits

I didn't come up with all of this on my own. I mentioned all the sources in the original gist.

// We register the TypeScript evaluator in gatsby-config so we don't need to do
// it in any other .js file. It automatically reads TypeScript config from
// tsconfig.json.
require('ts-node').register();
// Use a TypeScript version of gatsby-config.js.
module.exports = require('./gatsby-config.ts');
// All exported variables in this file will also used in gatsby-config.js.
export const siteMetadata = {
title: `My Gatsby Site`,
description: `An example site.`,
};
export const plugins = [
'gatsby-plugin-typescript',
'gatsby-plugin-postcss',
);
// Because we used ts-node in gatsby-config.js, this file will automatically be
// imported by Gatsby instead of gatsby-node.js.
// Use the type definitions that are included with Gatsby.
import { GatsbyNode } from 'gatsby';
import { resolve } from 'path';
export const createPages: GatsbyNode['createPages'] = async ({
actions,
graphql,
}) => {
const { createPage } = actions;
const allMarkdown: {
errors?: any;
data?: { allMarkdownRemark: { nodes: { fields: { slug?: string } }[] } };
} = await graphql(`
query allMarkdownQuery {
allMarkdownRemark(limit: 1000) {
nodes {
fields {
slug
}
}
}
}
`);
allMarkdown.data?.allMarkdownRemark.nodes.forEach(node => {
const { slug } = node.fields;
if (!slug) return;
// Type-safe `createPage` call.
createPage({
path: slug,
component: resolve(__dirname, '../src/templates/index.tsx'),
context: {
slug,
},
});
});
};
@kevingelion

This comment has been minimized.

Copy link

@kevingelion kevingelion commented Mar 26, 2020

This is awesome! thanks so much for putting this together. I do see that there is no type defined for anything being returned in allMarkdown, though:

const allMarkdown: {
    errors?: any;
    data?: unknown;
}
@JohnAlbin

This comment has been minimized.

Copy link
Owner Author

@JohnAlbin JohnAlbin commented Mar 28, 2020

I do see that there is no type defined for anything being returned in allMarkdown, though:

Oh! Good catch. I've updated the gist to add the type definition for the allMarkdown const.

@leyanlo

This comment has been minimized.

Copy link

@leyanlo leyanlo commented Apr 2, 2020

Thanks for sharing! I’m trying this out and running into an eslint error:

gatsby-config.js
  0:0  error  Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser.
The file does not match your project config: gatsby-config.js.
The file must be included in at least one of the projects provided

Have you seen something like this? Any tips on how to update my .eslintrc or tsconfig.json to handle gatsby-config.js?

I’ve tried adding include: ["gatsby-config.js"] or include: ["*.js", "*.ts", "src/**/*.ts", "src/**/*.tsx"] to tsconfig.json, but that does not resolve the error. I’ve added createDefaultProgram to parserOptions, which fixes the issue, but sounds like an anti-pattern.

UPDATE 2020-04-02

Seems like the lint issue is caused by gatsby-config.js and gatsby-config.ts sharing the same name. Renaming to gatsby-config-exports.ts resolves the issue.

@kara-ryli

This comment has been minimized.

Copy link

@kara-ryli kara-ryli commented Apr 22, 2020

This is pretty awesome. Thanks so much for doing this. One thing that I'm considering is moving my gatsby API files into src/gatsby-api/*, which allows me a pattern that looks a bit more obvious (to me, at least).

gatsby-node.js

module.exports = require('./src/gatsby-api/node').default;

src/gatsby-api/node.ts

const NodeAPI: GatsbyNode = {
  async createPages({ actions, graphql }) { ... }
  // other methods
};
export default NodeAPI;

And lastly,

src/typings/GraphQL.d.ts

declare namespace GraphQL {
  namespace Model {
    interface Post {
      html: string;
      frontmatter: {
        date: string;
        path: string;
        title: string;
      };
    }
  }

  namespace Query {
    interface Posts {
      allMarkdownRemark: {
        edges: Array<{
          node: Model.Post[]
        }>;
      };
    }
  }
}

Which integrates well with Gatsby's built-in typings:

    const result = await graphql<GraphQL.Query.Posts>(query);
    if (result.errors) {
      console.error(result.errors);
    }
    result.data?.allMarkdownRemark.edges.forEach(({ node }) => {
      createPage({
        path: node.frontmatter.path,
        component: resolve(`src/templates/post.tsx`),
        context: {},
      });
    });
@fzembow

This comment has been minimized.

Copy link

@fzembow fzembow commented May 18, 2020

I ran into some problems when setting this up, I ended up needing to set the following fields in my tsconfig.json in addition to installing @types/node

  "compilerOptions": {
    "module": "commonjs",
    "types": [
       "node"
    ]
  },
@ooloth

This comment has been minimized.

Copy link

@ooloth ooloth commented May 22, 2020

I ran into some problems when setting this up, I ended up needing to set the following fields in my tsconfig.json in addition to installing @types/node

  "compilerOptions": {
    "module": "commonjs",
    "types": [
       "node"
    ]
  },

I needed "module": "commonjs" as well. Thanks!

@dandv

This comment has been minimized.

Copy link

@dandv dandv commented May 22, 2020

Interesting, what problems have you guys run into? @ooloth, @fzembow?

@ooloth

This comment has been minimized.

Copy link

@ooloth ooloth commented May 22, 2020

When set to esnext, I was getting errors about trying to use ESM imports in a gatsby-* file (even though those were TS files). 🤷‍♂️

@dandv

This comment has been minimized.

Copy link

@dandv dandv commented May 22, 2020

Yeah, I gave up on esnext modules, tooling is just not there yet.

@jakebellacera

This comment has been minimized.

Copy link

@jakebellacera jakebellacera commented May 27, 2020

For those that use the compilerOptions.paths option in tsconfig.json, the ts-node module will not follow those path mappings. Instead, you need to require tsconfig-paths in your gatsby-config.js file as well:

require("ts-node/register")
require("tsconfig-paths/register")

module.exports = require("./gatsby-config-exports.ts")
@mokyox

This comment has been minimized.

Copy link

@mokyox mokyox commented Jun 3, 2020

Is anyone else getting issues an error Cannot query field "fields" on type "MarkdownRemark"?
I've tried rm -rf node_modules, restarting development servers and numerous other steps but I can't seem to find the fields field on my GraphiQL either.

Here's my files - repo link also here

gatsby-config.js


// We register the TypeScript evaluator in gatsby-config so we don't need to do
// it in any other .js file. It automatically reads TypeScript config from
// tsconfig.json.
/* eslint-disable */

require("ts-node").register();

// Use a TypeScript version of gatsby-config.js.
module.exports = require("./gatsby-config.ts");

gatsby-config.ts

export const plugins = [
  `gatsby-plugin-styled-components`,
  `gatsby-plugin-react-helmet`,
  {
    resolve: `gatsby-plugin-typescript`,
    options: {
      isTSX: true,
      jsxPragma: `React`,
      allExtensions: true,
    },
  },
  {
    resolve: `gatsby-source-filesystem`,
    options: {
      name: `blog`,
      path: `${__dirname}/src/content/blog`,
    },
  },
  `gatsby-transformer-remark`,
  `gatsby-transformer-sharp`,
  `gatsby-plugin-sharp`,
  {
    resolve: `gatsby-plugin-manifest`,
    options: {
      name: `Mo Jama`,
      start_url: `/`,
      background_color: `#1A202C`,
      theme_color: `#1A202C`,
      display: `minimal-ui`,
      icon: `src/assets/favicon.png`,
    },
  },
];

gatsby-node.ts

//https://gist.github.com/JohnAlbin/2fc05966624dffb20f4b06b4305280f9

// Because we used ts-node in gatsby-config.js, this file will automatically be
// imported by Gatsby instead of gatsby-node.js.

// Use the type definitions that are included with Gatsby.
import { GatsbyNode } from "gatsby";
import { resolve } from "path";

export const createPages: GatsbyNode["createPages"] = async ({
  actions,
  graphql,
}) => {
  const { createPage } = actions;

  const allMarkdown: {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    errors?: any;
    data?: { allMarkdownRemark: { nodes: { fields: { slug?: string } }[] } };
  } = await graphql(`
    query allMarkdownQuery {
      allMarkdownRemark(limit: 1000) {
        nodes {
          fields {
            slug
          }
        }
      }
    }
  `);

  allMarkdown.data?.allMarkdownRemark.nodes.forEach((node) => {
    const { slug } = node.fields;
    if (!slug) return;
    console.log(slug);

    // Type-safe `createPage` call.
    createPage({
      path: slug,
      component: resolve(__dirname, "../src/templates/blog-post.tsx"),
      context: {
        slug,
      },
    });
  });
};

Pretty stumped so far!

@jakebellacera

This comment has been minimized.

Copy link

@jakebellacera jakebellacera commented Jun 4, 2020

@mokyox you need to create the slug fields first. You can do this on the onCreateNode hook:

// gatsby-node.ts
import { GatsbyNode } from "gatsby"

export const onCreateNode: GatsbyNode["onCreateNode"] = ({
  node,
  getNode,
  actions,
}) => {
  const { createNodeField } = actions

  if (node.internal.type === "MarkdownRemark") {
    const slug = createFilePath({ node, getNode })

    createNodeField({
      node,
      name: "slug",
      value: slug
    })
  }
}

The createFilePath() action is provided by gatsby-source-filesystem and offers a couple options to generate the slug in case you want to change the base path (i.e. put it under a subfolder) or remove the trailing slash.

@mokyox

This comment has been minimized.

Copy link

@mokyox mokyox commented Jun 5, 2020

@jakebellacera

Thanks so much for that - completely missed that fact I need to use the onCreateNode hook from the gatsby-source-filesystem! 😅.

It works perfectly now. Thanks again!

@jakebellacera

This comment has been minimized.

Copy link

@jakebellacera jakebellacera commented Jun 7, 2020

@mokyox no worries! I'm still learning my way around Gatsby and that tripped me up as well! Glad that helped 😃

@dandv

This comment has been minimized.

Copy link

@dandv dandv commented Jun 8, 2020

So I've just learned that there's a plugin gatsby-plugin-ts-config that "write all of your config files in Typescript", and has gotten quite a bit of exposure in the issue about native TypeScript support for Gatsby.

@isaac-martin

This comment has been minimized.

Copy link

@isaac-martin isaac-martin commented Jun 11, 2020

Anyone have this working with path aliases? I keep getting build errors because it can't resolve some stuff

@henricazottes

This comment has been minimized.

Copy link

@henricazottes henricazottes commented Jun 18, 2020

It seems promising but can't get it working, I have this error running gatsby develop:

  TSError: ⨯ Unable to compile TypeScript:
  gatsby-node.ts:29:53 - error TS7006: Parameter 'node' implicitly has an 'any' type.
  29   allMarkdown.data?.allMarkdownRemark.nodes.forEach(node => {
                                                         ~~~~
  gatsby-node.ts:29:20 - error TS1109: Expression expected.
  29   allMarkdown.data?.allMarkdownRemark.nodes.forEach(node => {
                        ~
  gatsby-node.ts:41:5 - error TS1005: ':' expected.
  41   });
         ~

Which is weird cause VSCode shows me the inferred type when I move the pointer over the node argument:
image

@thepedroferrari

This comment has been minimized.

Copy link

@thepedroferrari thepedroferrari commented Aug 24, 2020

Thank you, it works!

@bsgreenb

This comment has been minimized.

Copy link

@bsgreenb bsgreenb commented Oct 5, 2020

Hey @JohnAlbin , wondering if you have any insight into this issue? I think it may be related to using ts-node/commonjs as required in this setup? https://stackoverflow.com/questions/64202523/exporting-global-styles-with-font-assets-from-a-typescript-commonjs-module

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment