TypeScript Node 環境を作る

以下リポジトリを参照していただきたい。

一応動く Jest もいける https://github.com/superyusuke/ts-webpack-import

GraphQL にした https://github.com/superyusuke/ts-abimp-apollo

import { music } from "src/music" のような絶対パスを用いた import を実現するためには webpack を用いる必要がある。絶対パスを用いた import が使用できない場合には import { music } from "../../src/music" といった相対パス特有の、階層をかなり移動するパス指定を行うことになる。これは基本的にプロダクションレベルのアプリケーション開発においては避けるべきだろう。このような絶対パスを用いた import をするためにはサーバー開発においても webpack を用いる必要がある。たとえ tsconfig.json に以下のような設定項目を書いても、実はこれは実行時に起きる読み込みエラーの解決はしていない。

tsconfig.json
"paths": {
      "src/*": ["src/*"]
    }

最小限の GraphQL Server

以下のコードは最小限の GraphQL Server である。

express 等を用いなくても ApolloServer 自体がサーバー機能を提供している。BFF を実装するのであればこれだけを用いればよい。

initial
const typeDefs = gql`
  type Query {
    getAuthor(id: Int!): Author
  }
  type Author {
    name: String
    id: ID
  }
`;

const resolvers = {
  Query: {
    getAuthor: () => ({ name: "nakanishi", id: "123" })
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers
});

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

export default server;

以下のコードではサーバーを作成している。typeDefs は schema と同じ意味で、これによって GraphQL server が返す値に関する型定義をする。typeDefs = schema によって型は明確になるが、実際にどんな値を返すのかは、ここでは定義できない。実際に返す値の定義は resolver によって行われる。これも server を作成する際に渡している。

server を作成する
const server = new ApolloServer({
  typeDefs,
  resolvers
});

typeDefs = schema と resolver

schema は、GraphQL サーバーが返す値の型定義の集合だ。

以下のコードが示すのは、Query のうち getAuthor というものは、引数として文字列型の id を受け取って、Author 型の値を返すということである。

typeDefs
import { ApolloServer, gql } from "apollo-server";

const typeDefs = gql`
  type Query {
    getAuthor(id: Int!): Author
  }
  type Author {
    name: String
    id: ID
  }
`;

const resolvers = {
  Query: {
    getAuthor: () => ({ name: "nakanishi", id: "123" })
  }
};

つまり以下のようなクエリを投げれるということであり、その結果として { name: "strings", id: "1234" } といった形式の値が取れるということを定義している。

対応するクエリ
query {
  getAuthor(id:1) {
    name
    id
  }
}

schema が型を定義しているわけだが、その実行結果は resolver に定義する。上記 resolver は、Query のうち getAuthor を実行した際の解決を定義する。resolver は関数として定義する。ここでは単純にオブジェクトを返している。このオブジェクトは schema で定義した Author と型が一致している。

resolver の関数、例えばここでは getAuthor に与えられている関数は最大4つの引数を用いることができる。このうち今重要なのは二番目に与えられる args である。ここには Query で渡した値が入ってくる。getAuthor クエリが受け取ることになっている id を resolver で用いることができる。

resolver
const typeDefs = gql`
  type Query {
    getAuthor(id: String!): Author
  }
  type Author {
    name: String
    id: ID
  }
`;

const resolvers: Resolvers = {
  Query: {
    getAuthor: (parent, args, context) => ({ name: "nakanishi", id: args.id })
  }
};

context から必要なデータを注入する

ApollorServer を作る際に context から外部のデータを resolver に注入することができます。https://www.apollographql.com/docs/apollo-server/data/data/#context-argument

注入する際にはオブジェクトを返す関数を渡します。

context から必要なデータを注入する
const resolvers = {
  Query: {
    getAuthor: (parent, args, context) => {
      return context.authors.find(author => author.id === args.id);
    }
  }
};

const authors = [{ id: "1", name: "nakanishi" }, { id: "2", name: "satou" }];

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: () => ({
    authors
  })
});

context は resolver の第三引数に渡され、アクセスすることができます。

Schema から型定義ファイルを作成し、Resolver で型の恩恵を受ける

必要なパッケージを入れる。

yarn add @graphql-codegen/cli --dev
yarn add @graphql-codegen/typescript @graphql-codegen/typescript-resolvers --dev

設定ファイルを置く

codegen.yml
schema: "./src/index.ts"
overwrite: true
generates:
  ./src/types/graphql.d.ts:
    config:
      useIndexSignature: true
      contextType: ./context#Context

    plugins:
      - typescript
      - typescript-resolvers

以下 npm script を実行する

"codegen": "gql-gen --watch"

すると schema から、resolover のための型定義を生成することができる。

どこへ書き出されるか。 以下で定義した場所へである。

codegen.yml
generates:
  ./src/types/graphql.d.ts:

さらに context にも型を与えることができる。 https://github.com/dotansimha/graphql-code-generator/blob/master/docs/plugins/typescript-resolvers.md

ドキュメントにも書いてあるが、コマンドによって生成される graphql.d.ts から相対的にファイル名を指定すること。ここでは context.d.ts を参照している。#Context によって Context 変数を参照している。

context の型を決める config
    config:
      useIndexSignature: true
      contextType: ./context#Context

つまり以下の Context という type を参照している。

context.d.ts
type Author = {
  name: string;
  id: string;
};

export type Context = {
  authors: Author[];
};

これで resolver ないで context を参照した際に、全て型がつく。

resolve の解決関係の型を補足する

index.ts
import { ApolloServer, gql } from "apollo-server";
import { Resolvers } from "src/types/graphql";

const typeDefs = gql`
  type Query {
    getAuthor(id: String!): Author
  }
  type Author {
    name: String!
    id: ID!
    books: [Book!]!
  }
  type Book {
    title: String!
  }
`;

const resolvers: Resolvers = {
  Query: {
    getAuthor: (parent, args, context) => {
      const authors = context.authors;
      return authors.find(author => author.id === args.id);
    }
  },
  Author: {
    books: () => {
      return [{ title: "book title" }];
    }
  }
};

const authors = [{ id: "1", name: "nakanishi" }, { id: "2", name: "satou" }];

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: () => ({
    authors
  })
});

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

export default server;
parent.ts
export type AuthorParent = {
  id: string;
  name: string;
};
codegen.yml
schema: "./src/index.ts"
overwrite: true
generates:
  ./src/types/graphql.d.ts:
    config:
      useIndexSignature: true
      contextType: ./context#Context
      mappers: # これが重要
        Author: ./parent#AuthorParent
    plugins:
      - typescript
      - typescript-resolvers

以下ドキュメントを参照

https://github.com/dotansimha/graphql-code-generator/blob/master/docs/plugins/typescript-resolvers.md#mappers---overwrite-parents-and-resolved-values

ようは、resolver で author を返す際に、そこにはない例えば books といった値は Book 用の resolver で解決させる時に、それが author には存在しないことを示さないといけない。そのために mapper で指定する。その設定を上記ファイルでおこなっている。

ただ上記の例でいえば books に余計なプロパティがあれば怒られるが、必要なものがあるかは判断されていないので注意。

BFF ように API ように値を使う