こういう2つの型を扱うことがあると思います
type Article = {id: string, value: number}
type ArticleDraft = {id: string | null, value: number}
ORM などで一度保存するまでidが振られない、みたいな時によくある型ですね。
これは簡単な例ですが、フィールドが多くなると同じような型を2つ書くのが面倒くさいし、何よりバグを仕込みそうなので、今回はなんとかして Article 型から ArtcileDraft 型を最小限の手数で生成したい、と思います。
案1: Draft を先に定義して、 extends して絞り込む
interface ArticleDraft {id: null | string, value: number}
interface Article extends ArticleDraft { id: string }
意図したとおりの2つの型が出来たのですが、発想の順序が逆です。先に Article を宣言してから ArtcileDraft を生成したいので、別の手段をとります。
案2: Conditional Type の型魔法で nullable に変換
export type Draft<T, D extends keyof T> = { [K in keyof T]: (K extends D ? T[K] | null : T[K]) };
// Example
type Foo = { id: string }
type FooDraft = Draft<Foo, 'id'>
// => {id: null | string}
Twitter であれこれしてたら @wonderful_panda 氏に教えてもらいました。
型のプロパティをイテレートして別の型を再構築しながら、第二型引数で渡した key を満たす型は nullable な型に変形しています。
これが便利なのは、 文字列の uniontype で複数の型を nullable にできること
type Article = {id: string, value: number}
type ArticleDraft = Draft<Article, 'id' | 'value'>
// => {id: string | null, value: number | null}
意味はわかるのですが、自分でこれをひねり出すことは出来ませんでした。とはいえイディオムの組み合わせなので、一度知ったら色々応用できそうなテクニックですね。
参考