ウェブサイト検索

TypeScriptで関数型の型パラメータ定義をどのように推論するか?


次の Next.js 関数型を拡張しようとしています:

export type GetStaticProps<
  P extends { [key: string]: any } = { [key: string]: any },
  Q extends ParsedUrlQuery = ParsedUrlQuery,
  D extends PreviewData = PreviewData
> = (
  context: GetStaticPropsContext<Q, D>
) => Promise<GetStaticPropsResult<P>> | GetStaticPropsResult<P>

そのコンテキスト(GetStaticPropsContext)上の3つのプロパティが、次のように undefined にならないように:

type UpdateContext<T> = T extends (context: infer Context extends GetStaticPropsContext) => infer Return
    ? (context: SelectiveRequiredNotUndefined<Context, "locale" | "locales" | "defaultLocale">) => Return
    : never;

さて、これを行うと、これらのコンテキストプロパティが決して undefined にならない新しい型を使用できます:

type MyGetStaticProps = UpdateContext<GetStaticProps>;
const getStaticProps: MyGetStaticProps = async (context) => { 
  // do stuff
}

問題は、MyGetStaticProps がもはやジェネリックではないということです。私はいつも代わりにこれを行うことができると知っています:

type MyProps = {
    someField: string
};

export const getStaticProps: UpdateContext<GetStaticProps<MyProps>> = async (context) => {
  // do stuff
}

しかし、むしろ、より冗長な構文を避けるために型パラメーターの定義を推測する方法がないかと思っていました。

まとめると、私は次のような形のものを探しています:type MyGetStaticProps=UpdateContext<GetStaticProps>; しかし、これが MyGetStaticProps<MyProps> をサポートするもので(GetStaticProps の型パラメータを推論するもの)

こちらは、例の完全に動作するPlaygroundリンクです

回答 #1

残念ながら、TypeScript は現在これらの種類の型(「高階型」として知られている)を表現することができません。詳細は microsoft/TypeScript#1213 を参照してください。

このブログ記事がそうしているように、あるいはこのコメントが示しているように、それらを実装しようとすることはできますが、ぎこちなく、HKTsができることを完全には表していません。

おそらく、別の方法を使うか、問題を再考するか、あるいは最初からいくつかのコード部分を再設計してそれを防ぐ方が良いでしょう。

要するに、JSの関数のように型を取得して変更し、再び型として出力することは本当にできません。

// type GetStaticProps<...> = ...;
function GetStaticProps(...) { ... }

// type UpdateContext<Func ...> = ...;
function UpdateContext(Func) {
    const NewFunc = (...args) => {
        args[0].prop = "something else";

        return Func(...args);
    };

    return NewFunc;
}

// type Updated = UpdateContext<GetStaticProps>;
const Updated = UpdateContext(GetStaticProps);

// type Result = Updated<...>; // but Updated can't be a generic, it's a result of another type.
const Result = Updated({ prop: "will change" });

jcalzからのいくつかのHKTコメント(あなたのためのより詳細な情報と潜在的な回避策付き):

  • TypeScriptにおける高階型関数?

  • TypeScriptでジェネリックの中でジェネリックを使用する

  • TypeScriptでジェネリックパラメータを別のジェネリックパラメータで拡張するにはどうすればよいですか?

回答 #2

問題

あなたがここで達成しようとしていることを理解するのにしばらく時間がかかりましたが、ようやくあなたの目標は次の通りだと理解したと思います。

  1. GetStaticPropsContext を精緻化して、context の特定のプロパティが必須かつ非ヌルであるようにする。
  2. 関数から返される props の型を自分で定義するのではなく、infer できるようにするため。

単独で #2 を解決することは簡単です -- #1 なしで -- Next.js がこのための InferGetStaticPropsType<T> ユーティリティ型をエクスポートしているからです。

export type InferGetStaticPropsType<T> = T extends GetStaticProps<infer P, any>
  ? P
  : T extends (
      context?: GetStaticPropsContext<any>
    ) => Promise<GetStaticPropsResult<infer P>>
  ? P
  : never

ソース

あなたの問題の核心は、#1を実装すると、この型がもはや機能しなくなるということです。これは、より広い引数を受け取る関数が、それらの引数のより狭い精緻化を受け取る関数をextendsするのに対し、その逆は成り立たないためです。

その原則の任意の例示:

// this is `true`
type A = ((arg: number | string) => any) extends ((arg: string) => any) ? true : false;
// this is `false`
type B = ((arg: number) => any) extends ((arg: number | string) => any) ? true : false;

もしあなたの getStaticProps 関数が洗練された context タイプしか受け入れられない場合、InferGetStaticPropsType<T> ユーティリティ型はもはや機能しません。なぜなら、あなたの getStaticProps はもはや基本的な GetStaticProps を extends していないからです。

export const getStaticPropsA = async (context: GetStaticPropsContext) => {
    return {
        props: {
            someField: 'yes'
        }
    }
};

// this is `{ someField: string; }`
type PropsA = InferGetStaticPropsType<typeof getStaticPropsA>


export const getStaticPropsB = async (context: SelectiveRequiredNotUndefined<GetStaticPropsContext, "locale" | "locales" | "defaultLocale">) => {
    return {
        props: {
            someField: 'yes'
        }
    }
};

// this is `never`
type PropsB = InferGetStaticPropsType<typeof getStaticPropsB>

解決策

戻り値の型を推論したいのであれば、関数全体に GetStaticProps のような型を割り当てるのは意味がありません。引数にだけ型を割り当てたいでしょう。その部分はすでに SelectiveRequiredNotUndefined ユーティリティ型で完了しています。

欠けているのは、戻り値の型を推論する能力です。そのためには、いくつかのコアの Next.js 型を修正する必要があります。

方法 1: モジュール拡張

locale、locales、およびdefaultLocaleプロパティは、すべてのアプリが国際化を使用するわけではないため、任意としてマークされています。しかし、もしあなたのアプリがこのルーティング情報をnext.config.jsファイルに含めている場合、これらのプロパティは常に存在すると考えても安全です。

TypeScriptのモジュール拡張を使用して、nextのGetStaticPropsContext型を変更できます。

import { PreviewData } from 'next';
import { ParsedUrlQuery } from 'querystring';

declare module 'next' {
    export type GetStaticPropsContext<Q extends ParsedUrlQuery = ParsedUrlQuery> = {
        params?: Q
        preview?: boolean
        previewData?: PreviewData
        locale: string
        locales: string[]
        defaultLocale: string
    }
}

これは簡単な解決策のはずですが、正しく設定するのが時には面倒なことがあります。私は、GetStaticPropsやInferGetStaticPropsTypeのような、これを使用する他の型もオーバーライドする必要はないだろうと思っていましたが、もしかしたら必要なのかもしれません?

この遊び場にはいくつかのエラーがありますが、正しい型は取得できます。

アプローチ2:ローカル型

モジュールを拡張するのがあまりに手間でうまく動作させるのが大変な場合は、すでに試していたこと、つまりNext.jsの型を自分で修正したコピーを作成する方法を続けることができます。Next.jsのバージョンをインポートする代わりに、私たち自身のローカルの型ファイルからインポートすることになります。

import { GetStaticPropsContext, GetStaticPropsResult } from 'next';
import { ParsedUrlQuery } from 'querystring';

export type MyGetStaticPropsContext<Q extends ParsedUrlQuery = ParsedUrlQuery> =
    GetStaticPropsContext<Q> & Required<Pick<GetStaticPropsContext, 'locale' | 'locales' | 'defaultLocale'>>;

export type MyGetStaticProps<
  P extends { [key: string]: any } = { [key: string]: any },
  Q extends ParsedUrlQuery = ParsedUrlQuery
> = (context: MyGetStaticPropsContext<Q>) => Promise<GetStaticPropsResult<P>>

export type MyInferGetStaticPropsType<T> = T extends MyGetStaticProps<infer P, any>
  ? P
  : T extends (
      context?: MyGetStaticPropsContext<any>
    ) => Promise<GetStaticPropsResult<infer P>>
  ? P
  : never

// ==== Example use:

export const getStaticProps = async (context: MyGetStaticPropsContext) => {
    context.locale
    //       ^? (property) locale: string
    context.locales
    //       ^? (property) locales: string[]
    context.defaultLocale
    //       ^? (property) defaultLocale: string
    context.preview // As an example of a property that doesn't get modified
    //       ^? (property) preview?: boolean | undefined
    return {
        props: {
            someField: 'yes'
        }
    }
};

type MyProps = MyInferGetStaticPropsType<typeof getStaticProps>
//   ^? type MyProps = { someField: string; }

TypeScript プレイグラウンドのリンク

関連記事