0
0

お互いの type を union type としますが、お互いの property を never にしたいとき

Last updated at Posted at 2024-07-23

はしがき

仕事をしながら、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 です。
}

目的

  1. 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 なので、使わなくてもエラーはない

テストの結果:
https://www.typescriptlang.org/play/?#code/JYOwLgpgTgZghgYwgAgBIQDYYPbIN4CwAUMsgBaY4BcyIArgLYBG0xpA7tlBgCY3ZMAVhARhiAX2KhIsRCgDKEANZxg+NsmxlVAT2w0AzmCigA5hoPLV-ISLFFJRYmB0AHFAFk6YOnAwBBAwM6KAgeAFEADwQMOh4IAB4AFQAaZABpAD5kAF5kDQAfZAAKPGQAbQAFZFBkJQg9GAyAXQB+GhAIADdoZHFkIqSASkKSsqqakDqG7Cakto7u3v6i9JGnIhd3ZC8fP0Dg0J5-V2AAJQgDV2wQS1yd718AoJCwqJi4xPQsbDTFFWAmWIxAQNyMyEgRgAan46BAaLsngdXsdThcrmCUHlCCRyJR9MgAIwpDScbh8fDiEm4rS6AkAckhYHpEmBRFBtzAEMuYBhsQgACYEY99i8jidzpdrrcsepcRQfjRiRpacA9DRGTz6dTSJYATQ8I5xEA

といっても、全部 or なっているので、簡潔に書けるかも

type MutualAssuredExclude<T, P> = 
  | { [P in keyof K & T]?: never } | T | P

全部 or だったのでこのように書いて ok です。しかし、{ [P in keyof P & T]?: never } これが {} を許可しますので、予想通りに動いていません。

一つずつエラーになる type をつくる

のおかげさまで、気づきました。

たしかに、{} も許可しているので、解決のながれをみながら、ロジックを考え直しました。

  • ロジックの要件
  1. generic T と P をもらう
  2. T と P だけのときは許可(T | P)
  3. 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からできそうです。

0
0
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0