投稿日: 2023-02-28
prisma-fabbricaというファクトリのライブラリを使って、次のようなテストコードを書いていました。
const user = await UserFactory.create(); const [post1, post2] = await PorstFactory.createList([ { user: { connect: user.id } }, { user: { connect: user.id } }, ])
noUncheckedIndexedAccess
をtrueにしている場合、post1
とpost2
がPost | undefined
になってしまう問題があります。PostFactory.createList
の戻り値がPost[]
型であるためです。
戻り値を[Post, Post]
にするべきなのかなと一瞬思いましたが、PrismaのcreateManyの戻り値が配列であることを考えると、ライブラリの責任の範囲を超えていそうです。そのため、使用側でなんとかすることにしました。
最近uhyoさんがts-array-lengthというライブラリを公開されていたことを思い出しました。ts-array-length
は、配列をタプルに絞り込むためのユーティリティです。
import { hasLength } from 'ts-array-length' // const arr: string[] if (hasLength(arr, 1)) { // arr: readonly [string, string] const str: string = arr[0]; }
hasLength
の実装は以下のようになっています。いわゆるユーザー定義型ガードです。
export function hasLength<T, N extends number>( arr: readonly T[], length: N, ): arr is ReadonlyArrayExactLength<T, N> { return arr.length === length; }
テストの中では条件分岐を書くよりアサーションをしたほうがいい気がするので、上記を参考にしてアサーション関数を作ります(まだ試せていない)。
export function assertHasLength<T, N extends number>( arr: readonly T[], length: N, ): asserts arr is ReadonlyArrayExactLength<T, N> { if (arr.length !== length) { throw new Error(`Array length is expected to be ${length}, but got ${arr.length}`); } }
これを使うと、初めのテストコードは以下のように修正できます。
const user = await UserFactory.create(); const posts = await PorstFactory.createList([ { user: { connect: user.id } }, { user: { connect: user.id } }, ]) assertHasLength(posts, 2); // post1: Post, post2: Post const [post1, post2] = posts;
さて、気になるのはReadonlyArrayExactLength<T, N>
型です。これは名前の通り、要素がTで長さがNのreadonlyなタプルを作る型です。
// readonly [string, string] ReadonlyArrayExactLength<string, 2>
実装は次のようになっていました。ユーティリティ型にあってもいいような気がするので、追加されると嬉しいですね。
ts-array-length/types.ts at main · uhyo/ts-array-length
export type ReadonlyArrayExactLength<T, N extends number> = N extends number ? number extends N ? readonly T[] : IsCertainlyInteger<N> extends true ? ReadonlyArrayExactLengthRec<T, N, readonly []> : readonly T[] : never; type ReadonlyArrayExactLengthRec< T, L extends number, Result extends readonly T[], > = Result["length"] extends L ? Result : ReadonlyArrayExactLengthRec<T, L, readonly [T, ...Result]>;
IsCertainlyInteger<N>
は、Nが非負整数のリテラル型であるかどうかを判定しています。これは、マイナスの数や少数を渡したときに無限ループを防ぐためのものです。
ts-array-length/typeUtils.ts at main · uhyo/ts-array-length
分かりやすくするために簡略化したものが以下です。
type Tuple<Value, Length extends number, Result extends Value[] = []> = Result['length'] extends Length ? Result : Tuple<Value, Length, [...Result, Value]> // Test: [string, string, string] type Test = Tuple<string, 3>
使われているのは、Array['length']
を使って再帰するテクニックです。1 | 2 | 3 | ...
のような型もこのテクニックを使うと作れます。
参考: Generate literal type for numbers range in TypeScript
最初に諦めましたが、PostFactory
の戻り値をタプルにする方法を考えてみます。そのためには、長さNのCreatePostInput
のタプルを受け取り、長さNのPost
のタプル返す関数を定義する必要があります。
引数をタプルとして受け取るには、Tを配列型として[...T]
のようにします。
function createList<T extends CreatePostInput[]>(inputs: [...T]): Tuple<Post, T['length']> { return inputs.map((input, index) => ({ ...input, id: index + 1})) as Tuple<Post, T['length']>; } // posts: [Post, Post] const posts = createList([{ title: 'test' }, { title: 'test' }])
参考: TypeScript 4.0で導入されるVariadic Tuple Typesをさっそく使いこなす - Qiita
array.map
の戻り値をタプルにする方法が分からなかったので、もし分かる方がいらっしゃったら教えていただけると助かります。
noUncheckedIndexedAccess
がtrueの場合は、ts-array-lengthを使うと配列からタプルへ絞り込める[...T]
とする(Tは配列型)