typescript - چرا `Subject & Array` برای یک امضا سازگار الزامی بود؟


من دریافت میکنم:

امضای اضافه بار با implement.ts(2394) سازگار نیست

در:

/** Iterate through an Array. */
export default function eachr<Value>(
    subject: Array<Value>,
    callback: IteratorCallback<typeof subject, number, Value>
): typeof subject

از کل این بخش:

export interface IteratorCallback<Subject, Key, Value> {
    (this: Subject, value: Value, key: Key, subject: Subject): void | boolean
}

/** Iterate through an Array. */
export default function eachr<Value>(
    subject: Array<Value>,
    callback: IteratorCallback<typeof subject, number, Value>
): typeof subject

/** Iterate through an Object. */
export default function eachr<RecordKey extends keyof any, Value>(
    subject: Record<RecordKey, Value>,
    callback: IteratorCallback<typeof subject, RecordKey, Value>
): typeof subject

/** Iterate through the subject. */
export default function eachr<RecordKey extends keyof any, Value>(
    input: Array<Value> | Record<RecordKey, Value>,
    callback: IteratorCallback<typeof input, RecordKey | number, Value>
): typeof input {
    if (Array.isArray(input)) {
        // Array
        const subject = input as Array<Value>
        for (let key = 0; key < subject.length; ++key) {
            const value = subject[key]
            if (callback.call(subject, value, key, subject) === false) {
                break
            }
        }
    } else {
        // Object
        const subject = input as Record<RecordKey, Value>
        for (const key in subject) {
            if (subject.hasOwnProperty(key)) {
                const value = subject[key]
                if (callback.call(subject, value, key, subject) === false) {
                    break
                }
            }
        }
    }

    // Return
    return input
}

میتوانستم با تغییر آن به این صورت آن را کارا کنم:

/** Iterate through an Array. */
export default function eachr<Subject extends Array<Value>, Value>(
    subject: Subject & Array<Value>,
    callback: IteratorCallback<typeof subject, number, Value>
): typeof subject

با این حال، من نمیفهمم چرا این مشکل را حل کرد. مشکل چه بود و چرا این تغییر باعث شد که مشکل از بین برود؟

آنچه برای من حتی شگفتانگیزتر است این است که اگر همان تغییر را روی یک تابع تکرارکننده شیء خالص اعمال کنم، باعث میشود که آن به شکست برسد:

/** Iterate through an Object. */
export default function eachrObject<
    Subject extends Record<RecordKey, Value>,
    RecordKey extends keyof any,
    Value
>(
    subject: Subject & Record<RecordKey, Value>,
    callback: IteratorCallback<typeof subject, RecordKey, Value>
): typeof subject {
    for (const key in subject) {
        if (subject.hasOwnProperty(key)) {
            const value = subject[key]
            // above fails with: Element implicitly has an 'any' type because type 'Record<RecordKey, Value>' has no index signature.ts(7017)
            // below fails with: Argument of type 'string' is not assignable to parameter of type 'RecordKey'.ts(2345)
            if (callback.call(subject, value, key, subject) === false) {
                break
            }
        }
    }
    return subject
}

در حالی که این کار میکند:

/** Iterate through an Object. */
export default function eachrObject<RecordKey extends keyof any, Value>(
    subject: Record<RecordKey, Value>,
    callback: IteratorCallback<typeof subject, RecordKey, Value>
): typeof subject {
    for (const key in subject) {
        if (subject.hasOwnProperty(key)) {
            const value = subject[key]
            if (callback.call(subject, value, key, subject) === false) {
                break
            }
        }
    }
    return subject
}

با این حال هر دو فرم برای تکرارگر آرایه به خوبی کار میکنند:

/** Iterate through an Array. */
export default function eachrArray<Subject extends Array<Value>, Value>(
    subject: Subject & Array<Value>,
    callback: IteratorCallback<typeof subject, number, Value>
): typeof subject {
    for (let key = 0; key < subject.length; ++key) {
        const value = subject[key]
        if (callback.call(subject, value, key, subject) === false) {
            break
        }
    }
    return subject
}

/** Iterate through an Array. */
export default function eachrArray<Value>(
    subject: Array<Value>,
    callback: IteratorCallback<typeof subject, number, Value>
): typeof subject {
    for (let key = 0; key < subject.length; ++key) {
        const value = subject[key]
        if (callback.call(subject, value, key, subject) === false) {
            break
        }
    }
    return subject
}

Subject extends Array<Value> برای سازگاری بارگذاری بیش از حد در تکرارگر آرایه ضروری بود، در حالی که Subject extends Record<RecordKey, Value> تکرارگر شیء را خراب میکند؟

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

پاسخ شماره ۱

صادقانه بگویم، این حجم زیادی است که باید به آن رسیدگی کرد، و فکر نمیکنم بتوانم دقیقاً پاسخ بدهم که چرا شما توانستهاید چیزها را درست کار کنید. به نظر من، امضای اضافه بار شما هر دو باید شکست بخورند. بیایید نگاهی به یک مثال بسیار ساده از اضافه بار/پیادهسازی بیندازیم:

function foo(x: string): void; // narrower, okay 
function foo(x: string | number | boolean): void; // wider, error
function foo(x: string | number): void {} // impl

توجه کنید که امضای اضافه بار دوم خطا میدهد که سازگار با امضای پیادهسازی نیست. این به این دلیل است که x در اضافه بار نوعی گستردهتر از x در پیادهسازی است. و اضافه بارها نیاز به نوعهای باریکتر دارند.

همچنین توجه کنید که به طور کلی (از زمانی که --strictFunctionTypes در TypeScript 2.6 معرفی شد) نوعهای تابع در نوع پارامترهای خود معکوسمتغیر هستند. این باعث رفتار زیر میشود:

type StringAccepter = (x: string) => void;
const helloAccepter: StringAccepter = (x: "hello") => {}; // error
const stringOrNumberAccepter: StringAccepter = (x: string | number) => {}; // okay

helloAccepter یک StringAccepter معتبر نیست زیرا "hello" از string باریکتر است، در حالی که stringOrNumberAccepter یک StringAccepter معتبر است زیرا string | number از string گستردهتر است. و بنابراین پارامترهای تابعی که گستردهتر میشوند، توابع خود را باریکتر میکنند و بالعکس:

function bar(cb: (x: "hello")=>void): void; // error, cb is too wide because x is too narrow
function bar(cb: (x: string | number)=>void): void; // okay, cb is narrower because x is wider 
function bar(cb: StringAccepter): void {} // impl

پس من انتظار دارم هر دو سربارگذاری شما شکست بخورند، چون نوع callback در امضای پیادهسازی (IteratorCallback<typeof input, RecordKey | number, Value>) در واقع باریکتر از هر کدام از نوعهای callback در امضای فراخوانی شما است.

در این مرحله، به جای این که سعی کنید راه حل ممکن خود را که شامل یک پارامتر نوع Subject اضافی است، بررسی کنید و بفهمید چرا بعضی چیزها کار میکنند و بعضی دیگر نه (که باعث سردرد من میشود... شاید یک باگ کامپایلر باشد؟ شاید هم نه؟، چه کسی میداند)، من به جای آن با راه حلی که پیشنهاد میدهم میروم... امضای پیادهسازی را واقعاً به اندازه کافی گسترده کنید تا از هر دو امضای فراخوانی پشتیبانی کند:

/** Iterate through an Array. */
export function eachr<Value>(
  subject: Array<Value>,
  callback: IteratorCallback<typeof subject, number, Value>
): typeof subject

/** Iterate through an Object. */
export function eachr<RecordKey extends keyof any, Value>(
  subject: Record<RecordKey, Value>,
  callback: IteratorCallback<typeof subject, RecordKey, Value>
): typeof subject

/** Iterate through the subject. */
export function eachr<RecordKey extends keyof any, Value>(
  input: Array<Value> | Record<RecordKey, Value>, 
  // here is the change
  callback: IteratorCallback<Array<Value>, number, Value> |
    IteratorCallback<Record<RecordKey, Value>, RecordKey, Value>
): typeof input {
  if (Array.isArray(input)) {
    // Array
    const subject = input as Array<Value>
    // a new assertion:
    const cb = callback as IteratorCallback<Array<Value>, number, Value>;
    for (let key = 0; key < subject.length; ++key) {
      const value = subject[key]
      if (cb.call(subject, value, key, subject) === false) {
        break
      }
    }
  } else {
    // Object
    const subject = input as Record<RecordKey, Value>
    // a new assertion:
    const cb = callback as IteratorCallback<Record<RecordKey, Value>, RecordKey, Value>;
    for (const key in subject) {
      if (subject.hasOwnProperty(key)) {
        const value = subject[key]
        if (cb.call(subject, value, key, subject) === false) {
          break
        }
      }
    }
  }

  // Return
  return input
}

تفاوت این است که پارامتر callback در امضای پیادهسازی یک اتحاد واقعی از انواع مشابه پارامتر callback در هر امضای فراخوانی است. علاوه بر این، خود پیادهسازی نیاز دارد تا یک تأیید محدودکننده برای callback به cb انجام دهد، تقریباً به همان روشی که شما قبلاً برای input به subject انجام میدهید.

حال باید کامپایلر راضی باشد. امیدوارم کمک کند؛ موفق باشید!

پاسخ شماره ۲

مشکل

این مربوط به نحوه کار کردن انواع اتحادی است. مشکل از آخرین (تجمعی) بارگذاری ناشی میشود:

callback: IteratorCallback<typeof input, RecordKey | number, Value>

چون input در اینجا از نوع Array<Value> | Record<RecordKey, Value> است، تعریف callback که به این صورت ساخته شده است، اجازه میدهد ۴ ترکیب ممکن وجود داشته باشد:

IteratorCallback<Array<Value>, RecordKey, Value>
IteratorCallback<Array<Value>, number, Value>
IteratorCallback<Record<RecordKey, Value>, RecordKey, Value>
IteratorCallback<Record<RecordKey, Value>, number, Value>

اما تنها ۲ مورد از آنها طبق تعریف اضافهبار قبلی شما معتبر هستند

راهحل

این میتواند با گفتن اینکه callback یکی از این دو نوع خواهد بود، اصلاح شود:

callback: IteratorCallback<Array<Value>, number, Value> | IteratorCallback<Record<RecordKey, Value>, RecordKey, Value> 

این مشکل امضای اورلود با پیادهسازی تابع سازگار نیست را برطرف میکند. با این حال، یک مسئله دیگر کشف شده است: تایپاسکریپت ارتباط بین نوع input ارائهشده و callback مربوط به آن را برقرار نمیکند. از آنجایی که آخرین اورلود هنوز از نوعهای اتحادیه استفاده میکند — دو نوع برای input و دو نوع برای callback — چهار سناریو وجود دارد که تایپاسکریپت فکر میکند ممکن است اتفاق بیفتد. به نظر میرسد محبوبترین راهحل موقت برای این مشکل، استفاده از تأیید نوع است.