尝试将GatsbyJS教程内容转换为TypeScript化
将教程4~8的内容转化为TypeScript格式。使用Gatsby中的数据。
通过使用名为gatsby-plugin-typegen的插件,可以很容易地为GraphQL类型生成代码,因此能够轻松地进行实现。(大部分参考了2020年的Gatsby.js TypeScript化,详见Zenn网站。)
请原谅关于细节的不足,为了使整个系统运行起来,最低限度的TS转化是必要的(当然,如果能提点意见会让我很高兴)。
项目产出

可以在TS化的情况下,用相同的图片启动http://localhost:9000/。
教程完成后的默认分支是用JavaScript编写的,请点击此处。
教程转换为TypeScript的完成形式分支,请点击此处。
步骤
-
- 搭建TypeScript环境
-
- 生成GraphQL类型
- 将所有页面转化为TypeScript
1. TypeScript 的环境配置
使用”tsc –init”命令创建了初始化的tsconfig.json文件。
npx tsc --init
接下来我会下载两个必要的typescript库。
yarn add -D typescript
yarn add gatsby-plugin-typegen // gatsby-config.jsのplugins[]へ追記
tsconfig.json文件的设置
最开始,将strict模式的选项移除。
{
"compilerOptions": {
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"lib": ["dom", "es2017"], /* Specify library files to be included in the compilation. */
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"outDir": "./build", /* Redirect output structure to the directory. */
"rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
"baseUrl": "src", /* Base directory to resolve non-absolute module names. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
// "strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
},
"include": ["src/**/*", "gatsby-node/index.ts"],
"exclude": ["node_modules", "public", "build", "src/templates/blog-post.tsx"],
}
将内容添加到gatsby-config.js文件中。
module.exports = {
siteMetadata: {
title: `typescriptのテスト gatsbyのチュートリアル参考:https://www.gatsbyjs.com/tutorial/part-four/`,
description: `これは説明文章ですよ`,
author: `gatsbyJSマン`,
},
plugins: [
`gatsby-plugin-emotion`,
{
resolve: `gatsby-plugin-typography`,
options: {
pathToConfigModule: `src/utils/typography`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `src`,
path: `${__dirname}/src/`,
}
},
`gatsby-transformer-remark`,
{
resolve: `gatsby-plugin-manifest`,
options: {
name: `GatsbyJS`,
short_name: `GatsbyJS`,
start_url: `/`,
background_color: `#6b37bf`,
theme_color: `#6b37bf`,
display: `standalone`,
icon: `src/images/icon.png`,
},
},
`gatsby-plugin-offline`,
`gatsby-plugin-react-helmet`,
`gatsby-plugin-typegen`, // 型生成のプラグインを追加
],
}
2. 生成GraphQL的类型
我可以从任何地方开始,但首先我会在index.js文件中尝试生成GraphQL的类型。
将文件名更改为index.tsx。显然,如果保持不变,会显示类型错误(使用的是VSCode编辑器)。
我将其改写为以下方式。
/** @jsx jsx */ //emotionCSSのTS化のための記述
import React, {FC} from "react"
import { jsx, css } from '@emotion/react' // jsxを追加
import { Link, graphql } from "gatsby"
import { rhythm } from "../utils/typography"
import Layout from "components/layout"
const Home: FC<{ data: any }> = ({data}): any => { //型生成するまで適当にanyを突っ込んでおく
return (
<Layout>
<div>
<h1
css={css`
display: inline-block;
border-bottom: 1px solid;
`}
>
Amazing Pandas Eating Things
</h1>
<h4>{data.allMarkdownRemark.totalCount} Posts</h4>
{data.allMarkdownRemark.edges.map(({ node }) => (
<div key={node.id}>
<Link
to={node.fields.slug}
css={css`
text-decoration: none;
color: inherit;
`}
>
<h3
css={css`
margin-bottom: ${rhythm(1 / 4)};
`}
>
{node.frontmatter.title}{" "}
<span
css={css`
color: #bbb;
`}
>
— {node.frontmatter.date}
</span>
</h3>
<p>{node.excerpt}</p>
</Link>
</div>
))}
</div>
</Layout>
)
}
export const query = graphql`
query MarkdownOfIndex { //便宜上、任意で型の名前をつけておく(ただし、名前がなくても生成されるファイルの type Query オブジェクトの中に型が格納されているので取り出せば良い)
allMarkdownRemark {
totalCount
edges {
node {
id
frontmatter {
title
date(formatString: "DD MMMM, YYYY")
}
fields {
slug
}
excerpt
}
}
}
}
`
export default Home
在这种情况下,
gatsby build
当你运行这样的操作后,会在src目录下生成一个”__ generated __”文件夹,其中包含了类型信息文件。
你可以通过文件搜索来找到”MarkdownOfIndex”,以确认生成的类型信息文件并将其提取出来。
type MarkdownOfIndexQueryVariables = Exact<{ [key: string]: never; }>;
type MarkdownOfIndexQuery = { readonly allMarkdownRemark: ( //この型を引っ張ってあげる
Pick<MarkdownRemarkConnection, 'totalCount'>
& { readonly edges: ReadonlyArray<{ readonly node: (
Pick<MarkdownRemark, 'id' | 'excerpt'>
& { readonly frontmatter: Maybe<Pick<MarkdownRemarkFrontmatter, 'title' | 'date'>>, readonly fields: Maybe<Pick<MarkdownRemarkFields, 'slug'>> }
) }> }
) };
使用Gatsby的PageProps库
回到 index.tsx 文件并添加 PageProps。
import { Link, graphql, PageProps } from "gatsby"
只要按照以下方式,将类型嵌入,就能够成功。
const Home: FC<PageProps<GatsbyTypes.MarkdownOfIndexQuery>> = ({data}) => {
return (
<Layout>
//以下略
3. 对src网页的所有内容进行TS转换。
按照与2相同的方法生成并应用类型。
建设之前的状态
尽量以最少的修改,将每个文件准备好可以进行构建的状态。如果在这个阶段有任何引起指责的地方,请随意添加“any”或其他方法来暂时解决。
/** @jsx jsx */
import React, {FC} from "react"
import { jsx, css } from "@emotion/react"
import { useStaticQuery, Link, graphql } from "gatsby"
import { rhythm } from "../utils/typography"
const Layout = ({ children }) => {
const data = useStaticQuery<GatsbyTypes.LayoutSiteMetadataQuery>(
graphql`
query LayoutSiteMetadata {
site {
siteMetadata {
title
}
}
}
`
)
return (
<div
css={css`
margin: 0 auto;
max-width: 700px;
padding: ${rhythm(2)};
padding-top: ${rhythm(1.5)};
`}
>
<Link to={`/`}>
<h3
css={css`
margin-bottom: ${rhythm(2)};
display: inline-block;
font-style: normal;
`}
>
{data.site.siteMetadata.title}
</h3>
</Link>
<Link
to={`/about/`}
css={css`
float: right;
`}
>
About
</Link>
{children}
</div>
)
}
export default Layout
import React from "react"
import PropTypes from "prop-types"
import { Helmet } from "react-helmet"
import { useStaticQuery, graphql } from "gatsby"
const SEO = ({ description, lang, meta, title }) => {
const { site } = useStaticQuery<GatsbyTypes.SEOsiteMetadataQuery>(
graphql`
query SEOsiteMetadata {
site {
siteMetadata {
title
description
author
}
}
}
`
)
const metaDescription = description || site.siteMetadata.description
return (
<Helmet
htmlAttributes={{
lang,
}}
title={title}
titleTemplate={`%s | ${site.siteMetadata.title}`}
meta={[
{
name: `description`,
content: metaDescription,
},
{
property: `og:title`,
content: title,
},
{
property: `og:description`,
content: metaDescription,
},
{
property: `og:type`,
content: `website`,
},
{
name: `twitter:card`,
content: `summary`,
},
{
name: `twitter:creator`,
content: site.siteMetadata.author,
},
{
name: `twitter:title`,
content: title,
},
{
name: `twitter:description`,
content: metaDescription,
},
].concat(meta)}
/>
)
}
SEO.defaultProps = {
lang: `en`,
meta: [],
description: ``,
}
SEO.propTypes = {
description: PropTypes.string,
lang: PropTypes.string,
meta: PropTypes.arrayOf(PropTypes.object),
title: PropTypes.string.isRequired,
}
export default SEO
import React from "react"
import { graphql } from "gatsby"
import Layout from "../components/layout"
const About = ({ data }) => {
return (
<Layout>
<h1>About {data.site.siteMetadata.title}</h1>
<p>
We are the only site running on your computer dedicated to showing the
best photos and videos of pandas eating lots of food.
</p>
</Layout>
)
}
export const query = graphql`
query AboutsiteMetadata {
site {
siteMetadata {
title
}
}
}
`
export default About
import React from "react"
import { graphql } from "gatsby"
import Layout from "../components/layout"
const MyFiles = ({data}) => {
console.log(data)
return (
<Layout>
<div>
<h1>My Sites Files</h1>
<table>
<thead>
<tr>
<th>relativePath</th>
<th>prettySize</th>
<th>extension</th>
<th>birthTime</th>
</tr>
</thead>
<tbody>
{data.allFile.edges.map(({ node }, index) => (
<tr key={index}>
<td>{node.relativePath}</td>
<td>{node.prettySize}</td>
<td>{node.extension}</td>
<td>{node.birthTime}</td>
</tr>
))}
</tbody>
</table>
</div>
</Layout>
)
}
export const query = graphql`
query {
allFile {
edges {
node {
relativePath
prettySize
extension
birthTime(fromNow: true)
}
}
}
}
`
export default MyFiles
import React from "react"
import { graphql } from 'gatsby'
import Layout from "../components/layout"
import SEO from "../components/seo"
const BlogPost = ({data}) => {
const post = data.markdownRemark
return (
<Layout>
<SEO title={post.frontmatter.title} description={post.excerpt} />
<div>
<h1>{post.frontmatter.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.html }}/>
</div>
</Layout>
)
}
export const query = graphql`
query($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
html
frontmatter {
title
}
excerpt
}
}
`
export default BlogPost
typography.js的文件名仅更改为typography.ts
gatsby build
将gatsby-node.js转化为TypeScript。
下一步是将gatsby-node.js转化为TS,因此需要安装ts-node。
yarn add -D ts-node
在将gatsby-node.js转换为TypeScript时,我们保留了gatsby-node.js文件名,并创建了一个名为gatsby-node/index.ts的用于TypeScript的文件夹和文件,并从这里引用。 参考:Gatsby.js的TypeScript化 2020
首先,将gatsby-node.js中的处理过程转化为TypeScript并添加到index.ts文件中。
import path from 'path'
import { createFilePath } from "gatsby-source-filesystem"
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, basePath: `pages` })
createNodeField({
node,
name: `slug`,
value: slug,
})
}
}
export const createPages: GatsbyNode["createPages"] = async ({ graphql, actions }) => {
const { createPage } = actions
const result = await graphql<{ allMarkdownRemark: GatsbyTypes.Query["allMarkdownRemark"]}>(`
{
allMarkdownRemark {
edges {
node {
fields {
slug
}
}
}
}
}
`)
const { data } = result || 'undefined';
if( data === undefined) throw 'データが見つかりませんでした';
data.allMarkdownRemark.edges.forEach(({node}) => {
if(node.fields){
createPage({
path: node.fields.slug || '/undefined',
component: path.resolve(`./src/templates/blog-post.tsx`),
context: {
slug: node.fields.slug
}
})
}
})
}
然而,gatsby-node.js将被重写为拉取gatsby-node/index.ts的代码。
"use strict"
require("ts-node").register({
compilerOptions: {
module: "commonjs",
target: "esnext",
},
})
require("./src/__generated__/gatsby-types")
const {
createPages,
onCreateNode,
} = require("./gatsby-node/index")
exports.createPages = createPages
exports.onCreateNode = onCreateNode
在这个时候,我建议先用 gatsby develop 来确认一下是否正常运行,我觉得转译应该是成功的(如果有错误的话,可以用任何方法来处理,比如使用any等等哈)。
以严格模式逐步修正
{
"compilerOptions": {
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"lib": ["dom", "es2017"], /* Specify library files to be included in the compilation. */
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"outDir": "./build", /* Redirect output structure to the directory. */
"rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
"baseUrl": "src", /* Base directory to resolve non-absolute module names. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"strictNullChecks": true, /* Enable strict null checks. */
"strictFunctionTypes": true, /* Enable strict checking of function types. */
"strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
"strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
},
"include": ["src/**/*", "gatsby-node/index.ts"],
"exclude": ["node_modules", "public", "build", "src/templates/blog-post.tsx"],
}
那么立刻就会遇到错误,所以我们需要进行修正。
因为在生成的undefined处出现错误,需要进行修正。

对组件和页面进行了修整看了看。
这里是git – 完整的教程的TS化分支在这里
/** @jsx jsx */
import { FC } from "react"
import { jsx, css } from "@emotion/react"
import { useStaticQuery, Link, graphql } from "gatsby"
import { rhythm } from "../utils/typography"
const Layout: FC = ({ children }) => {
const data = useStaticQuery<GatsbyTypes.LayoutSiteMetadataQuery>(
graphql`
query LayoutSiteMetadata {
site {
siteMetadata {
title
}
}
}
`
)
return (
<div
css={css`
margin: 0 auto;
max-width: 700px;
padding: ${rhythm(2)};
padding-top: ${rhythm(1.5)};
`}
>
<Link to={`/`}>
<h3
css={css`
margin-bottom: ${rhythm(2)};
display: inline-block;
font-style: normal;
`}
>
{data.site?.siteMetadata?.title}
</h3>
</Link>
<Link
to={`/about/`}
css={css`
float: right;
`}
>
About
</Link>
{children}
</div>
)
}
export default Layout
import React from "react"
// import PropTypes from "prop-types"
import { Helmet } from "react-helmet"
import { useStaticQuery, graphql } from "gatsby"
interface SEOTypes {
description?: string,
lang?: string,
meta?: any,
title: string,
}
const SEO = ({
description,
lang,
meta,
title
}: SEOTypes) => {
const { site } = useStaticQuery<GatsbyTypes.SEOsiteMetadataQuery>(
graphql`
query SEOsiteMetadata {
site {
siteMetadata {
title
description
author
}
}
}
`
)
const metaDescription = description || site?.siteMetadata?.description
if(!lang) lang = 'ja';
if(!meta) meta = {};
return (
<Helmet
htmlAttributes={{
lang,
}}
title={title}
titleTemplate={`%s | ${site?.siteMetadata?.title}`}
meta={[
{
name: `description`,
content: metaDescription,
},
{
property: `og:title`,
content: title,
},
{
property: `og:description`,
content: metaDescription,
},
{
property: `og:type`,
content: `website`,
},
{
name: `twitter:card`,
content: `summary`,
},
{
name: `twitter:creator`,
content: site?.siteMetadata?.author,
},
{
name: `twitter:title`,
content: title,
},
{
name: `twitter:description`,
content: metaDescription,
},
].concat(meta)}
/>
)
}
export default SEO
/** @jsx jsx */
import {FC} from "react"
import { jsx, css } from '@emotion/react'
import { Link, graphql, PageProps } from "gatsby"
import { rhythm } from "../utils/typography"
import Layout from "../components/layout"
const Home: FC<PageProps<GatsbyTypes.MarkdownOfIndexQuery>> = ({data}) => {
return (
<Layout>
<div>
<h1
css={css`
display: inline-block;
border-bottom: 1px solid;
`}
>
Amazing Pandas Eating Things
</h1>
<h4>{data.allMarkdownRemark.totalCount} Posts</h4>
{data.allMarkdownRemark.edges.map(({ node }) => (
<div key={node.id}>
<Link
to={node.fields?.slug || '/'}
css={css`
text-decoration: none;
color: inherit;
`}
>
<h3
css={css`
margin-bottom: ${rhythm(1 / 4)};
`}
>
{node.frontmatter?.title}{" "}
<span
css={css`
color: #bbb;
`}
>
— {node.frontmatter?.date}
</span>
</h3>
<p>{node.excerpt}</p>
</Link>
</div>
))}
</div>
</Layout>
)
}
export const query = graphql`
query MarkdownOfIndex {
allMarkdownRemark {
totalCount
edges {
node {
id
frontmatter {
title
date(formatString: "DD MMMM, YYYY")
}
fields {
slug
}
excerpt
}
}
}
}
`
export default Home
import React, {FC} from "react"
import { graphql, PageProps } from "gatsby"
import Layout from "../components/layout"
const About: FC<PageProps<GatsbyTypes.AboutsiteMetadataQuery>> = ({ data }) => {
return (
<Layout>
<h1>About {data.site?.siteMetadata?.title}</h1>
<p>
We are the only site running on your computer dedicated to showing the
best photos and videos of pandas eating lots of food.
</p>
</Layout>
)
}
export const query = graphql`
query AboutsiteMetadata {
site {
siteMetadata {
title
}
}
}
`
export default About
import React, {FC} from "react"
import { graphql, PageProps } from "gatsby"
import Layout from "../components/layout"
const MyFiles: FC<PageProps<GatsbyTypes.myFilesAllFileQuery>> = ({data}) => {
return (
<Layout>
<div>
<h1>My Sites Files</h1>
<table>
<thead>
<tr>
<th>relativePath</th>
<th>prettySize</th>
<th>extension</th>
<th>birthTime</th>
</tr>
</thead>
<tbody>
{data.allFile.edges.map(({ node }, index) => (
<tr key={index}>
<td>{node.relativePath}</td>
<td>{node.prettySize}</td>
<td>{node.extension}</td>
<td>{node.birthTime}</td>
</tr>
))}
</tbody>
</table>
</div>
</Layout>
)
}
export const query = graphql`
query myFilesAllFile {
allFile {
edges {
node {
relativePath
prettySize
extension
birthTime(fromNow: true)
}
}
}
}
`
export default MyFiles
import React, {FC} from "react"
import { graphql, PageProps } from 'gatsby'
import Layout from "../components/layout"
import SEO from "../components/seo"
const BlogPost: FC<PageProps<GatsbyTypes.blogPostRemarkQuery>> = ({data}) => {
const post = data.markdownRemark
return (
<Layout>
<SEO title={post?.frontmatter?.title || "undefined"} description={post?.excerpt || "undefined"} />
<div>
<h1>{post?.frontmatter?.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post?.html || "undefined"}}/>
</div>
</Layout>
)
}
export const query = graphql`
query blogPostRemark($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
html
frontmatter {
title
}
excerpt
}
}
`
export default BlogPost
添加缺少的库。
yarn add -D @types/react-helmet @types/typography
在这里,可悲的是,在GatsbyJS教程中使用的”typography-theme-kirkham”的@types文件找不到,因此无法进行TS编码。
进行开发时出现错误。
我想在这里进行gatsby develop的测试,但不知道为什么,如果保留”gatsby-plugin-typegen”插件,”组件生成的GraphQL类型将不会消失,终端将一直重新加载并无法运行。”
如果有人知道理由,请告诉我,因为我不明白。是因为只有在处理develop时,没有使用useStaticQuery的GraphQL才会重新生成类型吗?
当组件的GraphQL类型消失后,通过进行gatsby构建,就可以恢复其原始状态。
因此,在进行gatsby开发时,需要注释掉在gatsby-config.js中配置的gatsby-plugin-typegen。
我尝试构建并启动了服务。
gatsby build
gatsby serve

如果可以在 http://localhost:9000/ 上启动,则完成。