typescript - چگونه یک نوع عمومی را بر اساس نوع ویژگی یک شی محدود کنیم


من میخواهم یک نوع کمکی در تایپاسکریپت به نام ExtractOnProp ایجاد کنم که یک شیء را از نوع اتحادیه T استخراج کند که خصوصیت خاص آن Prop دارای نوع مورد نظر ValueType باشد.

type ExtractOnProp<T, Prop extends keyof T, ValueType> = /* ... */;

type Test1 = ExtractOnProp<
  | { value: "test1" | "test2", prop1: any }
  | { value: "test1", prop2: any }
  | { value: "test3", prop3: any },
  "value",
  "test1">;
// Test1 == { value: "test1", prop1: any } | { value: "test1", prop2: any };

// Real use case is more complicated but example usage:
// We want to find an element from an array of possible values
function narrow<
  T extends { value: string },
  Value extends readonly T['value'][],
>(
  elements: readonly T[],
  filter: Value,
): ExtractOnProp<T, 'value', Value[number]> {
  const result = elements.find((el) => filter.includes(el.value as any));
  if (!result) {
    throw new Error('not found');
  }
  return result as any;
}
narrow(
  [
    { value: 'test1', prop1: 1 },
    { value: 'test2', prop1: 2 },
    { value: 'test3', prop3: 3 },
  ] as const,
  ['test1', 'test3'] as const,
);
// { value: 'test1', prop1: 1 } | { value: 'test3', prop3: 3 }

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

type ExtractOnProp<T, Prop extends keyof T, ValueType> = T[Prop] extends ValueType ? T : never;
// Always returns never

// The closest I got was property type intersection
type IntersectPropType<T, Prop extends keyof T, ValueType> = T[Prop] extends infer S
  ? S extends ValueProp
    ? S
    : never
  : never;
type Test1 = IntersectPropType<{ value: 'test1' | 'test2' } | { value: "test3"; prop: any }, 'value', 'test2' | 'omitted'>;
// Test1 = 'test2';

ویرایش:

من حالا با محدود کردن اجتماع نزدیکتر شدهام، اما نه به خود نوع خاصیت.

type ExtractOnProp<T, Prop extends keyof T, ValueType> = T extends {
  [_ in Prop]: infer S;
}
  ? ValueType extends S
    ? T
    : never
  : never;

type Test = ExtractOnProp<
  { value: 'a' | 'c'; prop1: any } | { value: 'b'; prop2: any },
  'value',
  'a'
>;
// Test = { value: 'a' | 'c', prop1: any }

پاسخ شماره ۱

با توجه به نیازهای شما به همان صورتی که بیان شده است، یک رویکرد ممکن است این باشد:

type ExtractOnProp<T, K extends keyof T, V> =
    T extends unknown ? V extends T[K] ?
    { [P in keyof T]: P extends K ? T[P] & V : T[P] }
    : never : never

این یک نوع شرطی توزیعی در هر دو T و V است. این بدان معناست که عملیات در انواع اجتماع در T و V توزیع خواهد شد؛ T و V به اعضای اجتماع خود تقسیم میشوند، نوع برای هر عضو بهطور جداگانه ارزیابی میشود و سپس نتیجه دوباره به یک اجتماع ترکیب میشود. بنابراین ExtractOnProp<T1 | T2, K, V1 | V2> همانند ExtractOnProp<T1, K, V1> | ExtractOnProp<T2, K, V1> | ExtractOnProp<T1, K, V2> | ExtractOnProp<T2, K, V2> خواهد بود. شما قطعاً میخواهید این کار را با T انجام دهید، زیرا بخشی از هدف بررسی هر عضو اجتماع T بهطور جداگانه است؛ و اگر در T توزیع روی اجتماع انجام ندهید، در نهایت همه T بهطور همزمان بررسی میشوند که کمکی نخواهد کرد.

تیپهای شرطی تنها در این حالت توزیعی هستند که تیپ بررسی شده یک پارامتر تیپ عمومی باشد. تیپ V extends T[K] ? { [P in keyof T]: P extends K ? T[P] & V : T[P] } : never در T توزیعی نخواهد بود. برای اینکه چنین شود، میتوانیم آن را در T extends unknown ? (...) : never که به ظاهر بیفایده است، قرار دهیم. البته T از unknown مشتق خواهد شد؛ ما واقعاً نمیخواهیم آن را بررسی کنیم. در عوض، فقط میخواهیم T را به اعضای اجتماع آن تقسیم کنیم قبل از اینکه بقیه تیپ بررسی شود.

پس، بقیه نوع آن است

V extends T[K] ? { [P in keyof T]: P extends K ? T[P] & V : T[P] } : never

این باعث میشود که V نیز به اعضای اتحادیهٔ خود تقسیم شود. اگر عضو V قابل اختصاص به ویژگی در کلید K از T نباشد، پس میخواهیم این عضو T را از خروجی حذف کنیم (به همین دلیل never). اگر قابل اختصاص باشد، پس میخواهیم این عضو را بگیریم. اما به جای اینکه فقط T را برگردانیم، آن را به {[P in keyof T]: P extends K ? T[P] & V : T[P] } نگاشت میکنیم. این نوع اساساً همان T است به جز ویژگی در K، که با V تلاقی داده میشود. به این ترتیب اگر ویژگی گستردهتر از V باشد، محدود خواهد شد.


بیایید آن را امتحان کنیم:

type Test = ExtractOnProp<
    { value: "test1" | "test2", prop1: any } |
    { value: "test1", prop2: any } |
    { value: "test3", prop3: any },
    "value", "test1">
// type Test = 
//   { value: "test1";  prop1: any; } | 
//   { value: "test1";  prop2: any; }

خوبه، عضو "test3" کاملاً حذف شده، در حالی که عضو "test1" | "test2" کمی محدودتر شده است.

آزمون بعدی:

const ret = narrow([
    { value: 'test1', prop1: 1 },
    { value: 'test2', prop1: 2 },
    { value: 'test3', prop3: 3 },
] as const, ['test1', 'test3'] as const);
/* const ret: {
    readonly value: "test1";
    readonly prop1: 1;
    readonly prop3?: undefined;
} | {
    readonly value: "test3";
    readonly prop3: 3;
    readonly prop1?: undefined;
} */

دوباره، به نظر منطقی میآید. بیشتر:

type Test1 = ExtractOnProp<
    { value: 'test1' | 'test2' } |
    { value: "test3"; prop: any },
    'value', 'test2' | 'omitted'>;
// type Test1 = {  value: "test2"; }

خب، فکر کنم؟ آخری:

type Test2 = ExtractOnProp<
    { value: 'a' | 'c'; prop1: any } |
    { value: 'b'; prop2: any },
    'value',
    'a'
>;
// type Test2 = { value: "a"; prop1: any;}

باشه، همهی اینها کاری که فکر میکنم میخواهی انجام میدهند را انجام میدهند. این نوع دستکاریهای نوع همیشه به نظر میرسد که دارای موارد لبه زیادی هستند، بنابراین اگر یک ویژگی هم نباشد که به شکلی متفاوت از آنچه میخواهی عمل کند، تعجب میکنم. بنابراین به طور کامل آزمایش کن و امیدوارم اگر چیزی مطابق خواستهات نبود بتوانی خودت آن را اصلاح کنی.

لینک زمین بازی به کد