はしがき
仕事をしながら、api 取得からこのような jbuilder をみました。
if @hello?
json.hello 1
json.world hello_world
return
end
json.ohaiyo 'asa'
json.sekai ohaiyo_sekai
このような感覚なので、typescript は以下のように書きました。
interface Hello {
hello: number
world: object
}
interface Sekai {
ohaiyo: string
sekai: object
}
type ApiResponse = Hello | Sekai
単純に A|B
しても問題ありません。
しかし、以下のようなコードは許されたので、もっと完璧に許可したくなくて、細かく設定するようにしました。
interface Hello {
hello: number
world: object
}
interface Sekai {
ohaiyo: string
sekai: object
}
type ApiResponse = Hello | Sekai
const testValue: ApiResponse = {
hello: 1,
world: {},
ohaiyo: 'test' // 人の観点ではありえない property です。
}
目的
- union する type1, type2 のお互いの property を許可しないこと
結果
type PreventUnnessesaryPropertiesOf<T> = {
[K in keyof T]: Record<K, never> &
Partial<Record<Exclude<keyof T, K>, never>>;
}[keyof T];
type MutualAssuredExclude<T, K> =
| T
| K
| PreventUnnessesaryPropertiesOf<T & K>;
お互いの type の property を optional の never と設定、その上に自分の type の property を定義する、これを union type として、設定すれば、ok
interface Hello {
hello: number
world: object
}
interface Sekai {
ohaiyo: string
sekai: object
}
type PreventUnnessesaryPropertiesOf<T> = {
[K in keyof T]: Record<K, never> &
Partial<Record<Exclude<keyof T, K>, never>>;
}[keyof T];
type MutualAssuredExclude<T, K> =
| T
| K
| PreventUnnessesaryPropertiesOf<T & K>;
type MutualAssuredApiResponse = MutualAssuredExclude<Hello, Sekai>
const testValue: MutualAssuredApiResponse = {
hello: 1,
world: {},
ohaiyo: 'test' // error になる
}
const testValue2: MutualAssuredApiResponse = {
hello: 1,
ohaiyo: 'test', // error になる
sekai: {}
}
解決方法
generic type を A, B 取得したら A の場合は B の property を never、B の場合は A の property を never と設定することで解決できると思った。
type MutualAssuredExclude<T, P> =
| ({ [P in keyof K]: never } & T) // T を使う時 P の property を never
| ({ [P in keyof T]: never } & P) // P を使う時 T の property を never
optional ではないので、never の property が必ず必要
never と設定した property は必ず宣言する必要がある、しかし、それは never なので、宣言したらエラーが表示される
なので、以下のように optional に設定してエラーがならないようにしました。
type MutualAssuredExclude<T, P> =
| ({ [P in keyof P]?: never } & T)
| ({ [P in keyof T]?: never } & P)
const testValue: MutualAssuredExclude<Hello, Sekai> = {
ohaiyo: 'test',
sekai: {}
}
property が重複になったら、エラー
なんか、急に気になって、「property が重複になったら、どうするか?」なので、テストしました。
type MutualAssuredExclude<T, P> =
| ({ [P in keyof P]?: never } & T)
| ({ [P in keyof T]?: never } & P)
interface A {
a: number
b: number
c: number
}
interface B {
c: number
d: number
e: number
}
const testValue: MutualAssuredExclude<A, B> = {
a: 13, // error
b: 13, // error
c: 13, // error
}
property a, b, c が全部 never と言われました。
これ少し考えたら、{a: number, b: number, c: number} & {c?: never, d?: never, e?: never}
は c が never になるので、できないはずかなと思いました。
なので、union type を使って、お互いの type を使えなくなるようにしました。
type MutualAssuredExclude<T, P> =
| ({ [P in keyof P]?: never } | T)
| ({ [P in keyof T]?: never } | P)
さっきの A, B を使ったら、({ [P in keyof P]?: never } | T)
この部分は {a: number, b: number, c: number} | {c?: never, d?: never, e?: never}
になって、property a, b, c を使う時、 d, e を使ったら、never なので、エラー、また、optional condition なので、使わなくてもエラーはない
といっても、全部 or なっているので、簡潔に書けるかも
type MutualAssuredExclude<T, P> =
| { [P in keyof K & T]?: never } | T | P
全部 or だったのでこのように書いて ok です。しかし、{ [P in keyof P & T]?: never }
これが {}
を許可しますので、予想通りに動いていません。
一つずつエラーになる type をつくる
のおかげさまで、気づきました。
たしかに、{}
も許可しているので、解決のながれをみながら、ロジックを考え直しました。
- ロジックの要件
- generic T と P をもらう
- T と P だけのときは許可(
T | P
) - T もしくは P の property が一緒にある時は
never
にすること
1と2はすでに適用しています。
3は T のとき P の property は never
P のとき T の property は never
// hardcode 例
type HardcodePreventUnnessesaryProperties =
| { T1: never, T2?: never, T3?: never,...}
| { T1?: never, T2: never, T3?: never,...}
...
| { P1: any, P2: any, P3: any... } // P の事態は許可しますなので
であれば、お互いの property を全体 never にしましょう。
ある property は never にしますが、ほかの property は optional する必要があります。
// 1. 各 property を never にする
// 2. 各 property 以外の property は optional never にする
type PreventUnnessesaryPropertiesOf<T> = {
[K in keyof T]: Record<K, never> &
Partial<Record<Exclude<keyof T, K>, never>>;
}[keyof T];
// もしくは
type PreventUnnessesaryPropertiesOf<T> = {
[K in keyof T]: { K: never } &
Partial<{[L in Exclude<keyof T, K>]: never}>;
}[keyof T];
// comment で助言いただいたことをカッコで書いたら、どうかして、書いてみました。
これで、T もしくは P のお互いの property を一緒に使うときはエラーになります。
type MutualAssuredExclude<T, K> =
| PreventUnnessesaryPropertiesOf<T>
| T
| PreventUnnessesaryPropertiesOf<K>
| K
// また、これは union なので、以下のようにできます。
type MutualAssuredExclude<T, K> =
| PreventUnnessesaryPropertiesOf<T & K>
| T
| K
typescript 5.1 以前の version では?
5.1.6 playground
5.0.4 playground
また、助言をいただきましたが、
性能のために5.1からできそうです。