typescript - چرا یک اشتراک با یک اجتماع تمییز داده شده در یک نوع نگاشته شده به جای محدود کردن، یک اجتماع از همه انواع میدهد؟


من یک اجتماع تبعیضآمیز ساده مانند این دارم:

enum Kind {
    A = "a",
    B = "b",
    C = "c"
}

type Value =
    | { kind: Kind.A, value: 1 }
    | { kind: Kind.B, value: 2 }
    | { kind: Kind.C, value: 3 }

من میخواهم آن را به یک نوع شیء تبدیل کنم که هر کلید در Kind دارای یک مقدار متناظر از Value["value"] باشد. برای مثال بالا، باید شبیه { a: 1, b: 2, c: 3 } باشد.

برای انتخاب یک مقدار از یک اتحاد تمایز یافته، من از تقاطع با یک شیء دارای ویژگی نوع مشخص استفاده میکنم:

type t1 = (Value & { kind: Kind.A })["value"]
//   ^? type t1 = 1

اما وقتی همان کار را داخل یک نوع نگاشتشده انجام میدهم، بهجای آن خروجی Value["value"] میدهد:

type t2 = { [K in Kind]: (Value & { kind: K })["value"] }
//   ^? type t2 = { a: 1 | 2 | 3, b: 1 | 2 | 3, c: 1 | 2 | 3 }

اینجا چه چیزی را از دست دادهام؟

اینجا یک راهحل عملی برای مشکل من است:

type t3 = { [K in Kind]: (Value & { kind: K }) }

// This almost works, the main problem has magically disappeared by doing this in two iterations instead of one
// But now TS says that "value" cannot be used to index t3[K]
type t4 = { [K in Kind]: t3[K]["value"] }

// Actually works
type t5 = { [K in Kind]: t3[K] extends infer T extends { value: unknown } ? T["value"] : never }

همچنین مشکل نمایهگذاری را با ادغام ابتدا تقاطع حل کرد:

type Merge<T> = { [K in keyof T]: T[K] }

type t6 = { [K in Kind]: Merge<Value & { kind: K }> }
type t7 = { [K in Kind]: t6[K]["value"] } 

من مسئله را به موارد زیر محدود کردهام:

enum Kind {
    A = "a",
    B = "b",
    C = "c"
}

type Value =
    | { kind: Kind.A, value: 1 }
    | { kind: Kind.B, value: 2 }
    | { kind: Kind.C, value: 3 }


type t1 = Kind.A extends infer K extends Kind.A ? (Value & { kind: K })["value"] : never
//   ^? type t1 = 1 | 2 | 3

type t2 = (Value & { kind: Kind.A })["value"]
//   ^? type t2 = 1

ظاهراً TS با Kind.A بهطور متفاوت برخورد میکند اگر پارامتر نوع باشد (حتی اگر محدودیت معادل داشته باشد مانند مثال بالا).

چرا اینطور است؟

نمونه زمین بازی TS

پاسخ شماره ۱

این یک محدودیت طراحی در تایپاسکریپت است، همانطور که در microsoft/TypeScript#45428 توضیح داده شده است.

مشکل در ارجاع به یک نوع اجتماع مانند (Value & { kind: K })["value"] این است که کامپایلر متوجه نمیشود که باید اصلاً به {kind: K} مراجعه کند هنگام تعیین نوع ویژگی value، چون {kind: K} ویژگی value ندارد.

با توجه به نوعی مانند (A & B)[P] که در آن P توسعه یافته keyof A است اما keyof B نیست (و جایی که A & B فوراً ارزیابی نشده است)، کامپایلر مسیر کوتاه را انتخاب میکند و آن را فقط به صورت A[P] ارزیابی میکند. این تقریباً همیشه کار درست است، اما شما موقعیتی پیدا کردهاید که درست نیست. یا حداقل جایی که به وضوح درست نیست.

شما چه میگویید {foo: never, bar: 2}["bar"] باید باشد؟ به نظر میرسد باید 2 باشد، اما چه میشود اگر قانونی وجود داشته باشد که {foo: never, bar: 2} را به never کاهش دهد چون نمیتواند وجود داشته باشد؟ سپس شاید باید باشد (never)["bar"] که همان never است. میتوان بحث کرد که میتواند یا 2 باشد یا never بسته به اینکه قانون کاهش چه زمانی باید اعمال شود. حالت شما معادل این است، فقط با یک اتحاد از این نوعها: ({foo: "a", bar: 1} | {foo: never, bar: 2})["bar"] میتواند یا 1 | 2 باشد یا فقط 1 بسته به اینکه قانون کاهش چه زمانی اعمال شود.

اما حتی اگر تصمیم بگیریم که باید هرگز یا 1 باشد، کامپایلر زحمت نمیکشد تا این را پیدا کند، زیرا انجام این کار باعث میشود ارزیابی نوعها همیشه پرهزینهتر باشد، تنها برای بهبود گاهبهگاه صحت. بنابراین این یک محدودیت طراحی TypeScript است. راهحل موقت این است که از نوع کمکی Extract همانطور که در مسئله GitHub و پاسخ دیگر به سوال توضیح داده شده است، استفاده شود.

پاسخ شماره ۲

نوع ابزار Extract<Type, Union> میتواند در اینجا مفید باشد:

یک نوع میسازد با استخراج از Type همه اعضای اتحاد که قابل اختصاص به Union هستند.

نمونه

type T0 = Extract<"a" | "b" | "c", "a" | "f">;
//    ^? type T0 = "a"
type T1 = Extract<string | number | (() => void), Function>;
//    ^? type T1 = () => void

میتوان آن را به مثال شما به این صورت اعمال کرد:

محیط آزمایشی TS

enum Kind {
  A = "a",
  B = "b",
  C = "c",
}

type Value =
  | { kind: Kind.A; value: 1; }
  | { kind: Kind.B; value: 2; }
  | { kind: Kind.C; value: 3; };

type KindValueMap = { [K in Kind]: Extract<Value, { kind: K }>["value"] };
   //^? type KindValueMap = { a: 1; b: 2; c: 3; }


به طور جایگزین، میتوانید یک نوع نگاشته شده از هر عضو اجتماع ایجاد کنید، زیرا هر کدام تمام اطلاعات مورد نیاز را دارند:

محیط آزمایشی TS

type KindValueMap = { [V in Value as V["kind"]]: V["value"] };
   //^? type KindValueMap = { a: 1; b: 2; c: 3; }