header image

ぐっちー

経済学部の大学生です。ゲームをしたり本を読んだりプログラミングをしたりしています。C#とかReactとかGatsby.jsが好きです。

TypeScriptでオブジェクトのプロパティを上書きする型を作った

2022-11-11

TypeScript

例えば

type Hoge = {
  fuga: 'fugafuga';
  piyo: unknown;
};

のような型があったときにpiyoの型を上書きしてくれる型が必要だった。バグの原因になりそうではあるのでなるべく避けたほうが良さそう?

作ったもの

type Override<T, K extends { [P in keyof T]?: K[P] }> = Omit<T, keyof K> & K;

Tを型Kで上書きする。TKの部分型もどき(プロパティの名前だけ共通していてプロパティの型は同じとは限らない)。Omit<T, keyof K>Tから上書きしたいプロパティを取り除いた型を作り、それをKとのインターセクション型で拡張。

というのを作ってから調べてみたら似たようなことをされている方がいらっしゃった。(https://qiita.com/ibaragi/items/2a6412aeaca5703694b1)

なぜ必要だったか

Gatsby.js のお話。GraphQL Typegen でクエリの結果の型を生成したが gatsby-plugin-image のgatsbyImageDataの型がIGatsbyImageDataではなくRecord<string, unknown>で生成されていた。それが原因でgetImageだったりgetSrcに引数としてクエリの結果を渡せなかった。そこで、生成された型を上書きして新しい型を作ることにした。

query BlogPage {
  file(name: { eq: "hogehoge" }) {
    childImageSharp {
      gatsbyImageData(height: 600, width: 600)
    }
  }
  hokanimo {
    iroiro {
      arimasu
    }
  }
}

みたいなクエリに対して

declare namespace Queries {
  // ...
  type BlogPageQuery {
    readonly file: {
      readonly childImageSharp: {
        readonly gatsbyImageData: Record<string, unknown>
      } | null
    } | null;
    readonly hokanimo: {
      readonly iroiro: {
        readonly arimasu: string | null
      } | null
    } | null
  }
  // ...
}

みたいな型が GraphQL Typegen で生成される。これを

type ImageFileNode = {
  file: {
    childImageSharp: {
      gatsbyImageData: import('gatsby-plugin-image').IGatsbyImageData;
    };
  };
};

const BlogTemplate: React.FC<
  PageProps<Override<Queries.BlogPageQuery, ImageFileNode>>
> = ({ data }) => {
  // ...
};

というような感じで使うとうまくいく。

ドキュメントには gatsby-plugin-image についても正しい型が生成されるみたいなことが書いてあるはずなんですけどね(https://www.gatsbyjs.com/docs/how-to/local-development/graphql-typegen/#tips)。後で調べます。

終わりに

TypeScript、概ねいいけどTypeScript以外の部分?に型を与えるのが大変そう。

参考にしたもの

この投稿をシェア:
© 2021 ぐっちー