投稿日: 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は配列型)