typescript - چگونه میتوانم نوعی داشته باشم که Partial باشد اما مراجع ارسالی فقط شامل کلیدهای Foo باشند


فرض کنیم من دارم

class Foo {
   readonly a!: string
   readonly b!: string
   readonly c!: string
}

سپس متدی با پارامتر با ایجاد میکنم

  Partial<Foo>

مشکل این است که من هم دارم

class Bar {
   readonly a!: string
   readonly d!: string
}

من میتوانم Bar را به متدی که Partial<Foo> میگیرد پاس دهم، اما این چیزی نیست که قصد دارم.

آنچه من میخواهم، نوعی AllowOnlyKeysFrom<Partial<Foo>> است، شما هنوز هم میتوانید ۰ کلید، یا ۱ کلید، یا همه کلیدها را داشته باشید، اما Bar مطابقت نخواهد داشت زیرا کلیدی دارد که در foo نیست.

من سعی کردهام با چندین کتابخانه کار کنم، از جمله ts-essentials، ts-typedefs و ts-toolbelt اما نتوانستهام راهحلی پیدا کنم.

آیا راهی وجود دارد تا رفتار مورد نظرم را بدست آورم به طوری که کامپایلر مانع از ارسال اشیاء کاملاً بد به mymethod( part: Partial<Foo> ) شود؟

پاسخ شماره ۱

شما میخواهید نوعهای دقیق، که واقعاً در TypeScript وجود ندارند. نوعهای شیء در TypeScript به عنوان "باز" یا "قابل گسترش" در نظر گرفته میشوند؛ نوعی مانند {a?: string, b?: string, c?: string} تنها مشخص میکند که چه نوع ویژگیهایی در کلیدهای a، b و c وجود دارند. این هیچگونه محدودیتی برای کلیدهای دیگر ندارد. بنابراین مقداری مانند {a: "", b: "", c: "", d: 12345} معتبر است. و بنابراین Bar یک Partial<Foo> معتبر است.

توجه داشته باشید که این به طور کلی یک ویژگی مطلوب است، زیرا اجازه میدهد تا interface و class گسترش یابند:

interface X {
    x: string;
}
interface Y extends X {
    y: number;
}

اینجا، Y extends X به این معنی است که هر Y یک X است. این مستقیماً به نوع گسترش خصوصیات اضافی منجر میشود که شما از آن راضی نیستید:

const y: Y = { x: "", y: 1 }
const x: X = y; // okay

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

راهحل جایگزین مورد علاقه من این است که myMethod() را در نوع Partial<Foo>-مانند T که part خواهد بود، عمومی (generic) کنیم. میتوانید T را محدود کنید به طوری که هر ویژگی شناختهشده در آن باید یک ویژگی از Foo باشد، و اگر ویژگیهای اضافی وجود داشته باشد، باید never باشد. مانند این:

mymethod<T extends { [K in keyof T]: K extends keyof Foo ? Foo[K] : never }>(
    part: T
) { 
    console.log(part);
}

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

obj.mymethod({a: ""}) // okay

const bar: Bar = { a: "", d: "" };
obj.mymethod(bar); // error!
// Types of property 'd' are incompatible.

هورا، این از ایجاد Bar جلوگیری میکند! و این عالی است اگر اشیایی را به mymethod بدهید که مشخص است که خواص اضافی دارند. اما هیچ چیزی جلوی این را نمیگیرد:

const partialFoo: Partial<Foo> = bar; // valid assignment
obj.mymethod(partialFoo); // no error!

در اینجا، partialFoo بهطور صریح به عنوان یک Partial<Foo> مشخص شده است، و bar به آن اختصاص داده میشود. این موفقیتآمیز است زیرا نوعها در TypeScript باز هستند و دقیق نیستند. و کامپایلر کاملاً فراموش کرده است که partialFoo از یک Bar آمده است؛ او فقط آن را به عنوان Partial<Foo> میبیند. بنابراین کلیدهایی که برای کامپایلر شناخته شده هستند مشکلی ندارند، و کلیدی که باعث خطا است برای کامپایلر ناشناخته است، پس آن را تشخیص نمیدهد.

در این مرحله، من میگویم ممکن است بخواهید یک قدم عقبتر بروید و کار کردن با سیستم نوع TypeScript را به جای مخالفت با آن در نظر بگیرید. اگر TypeScript نوعهای شیء را باز در نظر بگیرد، کد شما نیز باید همین کار را انجام دهد، با عمل کردن صریح تنها روی کلیدهای شناختهشده. تصور کنید که ما یک تابع pluck() داریم که شیءای تولید میکند که تنها شامل کلیدهایی است که ما مشخص کردهایم:

function pluck<T, K extends keyof T>(t: T, ...k: K[]): Pick<T, K> {
    return k.reduce((a, k) => (k in t && (a[k] = t[k]), a), {} as Pick<T, K>);
}

و سپس یک نسخه از mymethod() بسازید که قبل از عمل کردن روی آن، ویژگیهای a، b و c را از part pluck کند:

mySafeMethod(part: Partial<Foo>) {
    this.mymethod(pluck(part, "a", "b", "c"));
}

در این حالت، دیگر مشکلی ندارد اگر یک Bar وارد کنید:

obj.mySafeMethod(bar); // { a: "" };

کد زمان اجرا انتظار دارد که part ممکن است خصوصیات بیشتری نسبت به a، b و c داشته باشد و صریحاً آنها را نادیده میگیرد، به جای اینکه، برای مثال، همه کلیدهای موجود را با Object.keys() یا for..in تکرار کند. این ممکن است از نظر جاوااسکریپت کمتر رایج باشد، اما باعث میشود کار با کامپایلر آسانتر شود.

باشه، امیدوارم یکی از این ایدهها بهت کمک کنه؛ موفق باشی!

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

بهروزرسانی

TypeScript واقعاً نوعهای دقیق را «درک» نمیکند، بهویژه نوعهای عمومی تعییننشدهای مانند چیزی که در پیادهسازی mymethod میبینید. بنابراین داخل mymethod کامپایلر چیز زیادی درباره part نمیداند. راههایی برای مقابله با این وجود دارد... یک روش زشت این است که نوع دقیق را با نوع باز تقاطع دهید تا کامپایلر حداقل بداند که T هم Partial<Foo> است و هم چیزهای دیگری که نمیتواند تأیید کند:

mymethod<T extends Partial<Foo> & 
  { [K in keyof T]: K extends keyof Foo ? Foo[K] : never }
>(
    part: T
) {
    console.log(part);
    part.b?.toUpperCase(); // okay
}

این برای تو کار میکند؟

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

پاسخ شماره ۲

@jcalz جواب عالی است، اما همان چیزی نیست که نهایتاً پیادهسازی کردم، هرچند که مرا به آنجا رساند.

type FooPart = Opaque<Partial<Foo>>;

در این مورد، Opaque از type-fest میآید و شما را مجبور میکند که هنگام ایجاد آن مشخص کنید که آن نوع را میخواهید، و فقط اجازه میدهد کلیدهای تعریفشده در آن نوع استفاده شوند.

foo(): Foo { 
   return { a: 'test' } as FooPart;
}

کجا

foo(): Foo { 
   return { a: 'test', d: 'not valid' } as FooPart;
}

شکست خواهد خورد.