• typescript

TypeScript で Result っぽいものを型で表現しようとして詰まっているお話

Rust でいう Result<T, E> が欲しくなり型を考えてみたんですけど詰まってるからとりあえず吐き出しちゃいます。自分のブログだし。


コード

Rust にならってこんな型を用意します。

// result.ts

export type Ok<T> = { type: 'ok'; value: T };
export type Err<E extends Error> = { type: 'err'; error: E };
export type Result<T, E extends Error> = Ok<T> | Err<E>;

で、 User defined type guard 使って便利関数も用意します。

export const isOk = <T, E extends Error>(r: Result<T, E>): r is Ok<T> => r.type === 'ok';
export const isErr = <T, E extends Error>(r: Result<T, E>): r is Err<E> => r.type === 'err';

そうするとこういうことができますよね。

// 参考値を作成する関数
// ややこしくしたいので無駄に async
async function getResult(): Promise<Result<number, Error>> {
  const result = await new Promise<number>((resolve) => setTimeout(() => resolve(Math.random()), 1000)).catch(
    () => new Error('timeout'),
  );

  return typeof result !== 'number'
    ? { type: 'err', error: result }
    : result < 0.5
    ? { type: 'err', error: new Error(`Invalid range: ${Math.random()}`) }
    : { type: 'ok', value: result };
}

async function main() {
  const r = await getResult();

  if (isOk(r)) {
    console.log(r.value, r.details);
    // console.error(r.error); // type error
  } else {
    // console.log(r.value); // type error
    console.error(r.error, r.details);
  }
}

おほほ、大変よろしですね 😊(ここまでは)

ここからが悩み

実戦では必ずまとめて処理したくなるユースケースがあると思うんですよね。

async function main() {
  const results = await Promise.all([getResult(), getResult()]);

  // 👇👇👇👇👇👇👇👇👇👇👇👇
  if (isEveryOk(results)) {
    console.log(results.map((r) => r.value));
  } else {
    console.error(results.map((r) => r.error));
  }
}

この isEveryOk をどう定義したものかがわからないです。 一応ここまでは頑張ったってやつを書きますと…

/** Result<T, E>[] を Ok<T>[] または Err<E>[] に変換する */
type ExtractResults<T extends Result<any, Error>[], A extends 'ok' | 'err'> = {
  [K in keyof T]: T[K] extends Result<infer P, infer Q> ? (A extends 'ok' ? Ok<P> : Err<Q>) : never;
};

/* エラー

A type predicate's type must be assignable to its parameter's type.
  Type 'ExtractResults<T, "ok">' is not assignable to type 'T'.
    'ExtractResults<T, "ok">' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Result<any, Error>[]'.
      Type 'T[K] extends Result<infer P, infer Q> ? Ok<P> : never' is not assignable to type 'T[K]'.
        Type 'Ok<unknown>' is not assignable to type 'T[K]'.ts(2677)

トノコト
 */
const isEveryOk = <T extends Result<any, Error>[]>(t: T): t is ExtractResults<T, 'ok'> =>
  t.every((r) => r.type === 'ok');

う〜ん、どうしたら良いのでしょうか…。 かなり惜しいところまでいった(つもり)ので、なんとかいい感じにしたいなぁと思ってますね。