تایپاسکریپت - محدود کردن با شرط بر اساس extends keyof T
من به بررسی موضوع 'نوع ایندکس keyof در TypeScript خیلی گسترده است' پرداختهام، اما نتوانستم بفهمم آیا و چگونه این مسئله به مورد زیر اعمال میشود:
type Foo<K, T> = K extends keyof T ? T[K] : never;
function test1<K, T>(foo: Foo<K, T>) { }
function test2<K extends keyof T, T>(foo: T[K]) {
test1(foo);
// ^ Argument of type 'T[K]' is not assignable to parameter of type 'Foo<K, T>'
}
اما چرا؟ ما میدانیم که K شاخص T است زیرا keyof T است.
درست است که keyof T میتواند string | number | symbol باشد، اما در این تنظیم خاص، همچنان قابل تخصیص به Foo<K, T> خواهد بود.
پیام خطای کامل:
Argument of type 'T[K]' is not assignable to parameter of type 'Foo<K, T>'.
Type 'T[keyof T]' is not assignable to type 'Foo<K, T>'.
Type 'T[string] | T[number] | T[symbol]' is not assignable to type 'Foo<K, T>'.
Type 'T[string]' is not assignable to type 'Foo<K, T>'.(2345)
برای زمینه، این در طول مورد آزمایشی زیر از @types/node مطرح شد:
interface T {
myEvent: [string]
}
type Listener<K> = K extends keyof T ? (
(...args: T[K]) => void
)
: never;
function on<K>(event: K, listener: Listener<K>): void {
emitter.on(event, listener);
}
در بالا، داشتن on<K extends keyof T> نیاز به داشتن شرط نوع در Listener را از بین نمیبرد، احتمالاً به دلیل همان محدودیت محدودسازی مشابه.
پاسخ شماره ۱
تحلیل
اما چرا؟ ما میدانیم که K شاخص T است چون keyof T است.
این فرض باعث خطایی است که دریافت میکنید. بین مورد شما و نمونهای از Node تفاوتی ظریف اما مهم وجود دارد.
بیایید ابتدا نگاهی به مورد دوم بیندازیم. در اینجا، T یک اینترفیس مشخص (تعریف شده) است که یک ویژگی واحد دارد به نام myEvent که خود یک تاپل ([string]) است.
برای درک بهتر تفاوت معنایی T، میتوانیم keyof T را به یک نوع نامگذاریشده استخراج کنیم و در همین حال، T را به MyInterface تغییر نام دهیم.
interface MyInterface {
myEvent: [string]
}
type MyKey = keyof MyInterface;
// equivalent to: type MyKey = "myEvent"
type Listener<K> = K MyKey ? (
(...args: T[K]) => void
)
: never;
function on<K>(event: K, listener: Listener<K>): void {
emitter.on(event, listener);
}
Listener< K > به نوع خاصی (رابط) MyInterface و اجرای آن بستگی دارد. TypeScript می تواند استنباط کند که MyKey ترکیبی از مقادیر رشته است (مانند رشته)
در موردی که شما با خطا مواجه میشوید، T یک پارامتر نوع بدون محدودیت است، که برای آن مقدار keyof T ممکن است یک اجتماع (union) باشد یا نباشد. بیایید نگاهی دقیقتر به keyof بیندازیم
type KeyOfNull = keyof null;
// = never, even though (typeof null === 'object')
type KeyOfUndefined = keyof undefined;
// = never;
type KeyOfString = keyof "constant string";
// = number | "fixed" | "sub" | "slice" | "small" | "bold" | "repeat" | typeof Symbol.iterator | "charAt" | "charCodeAt" | "concat" | "indexOf" | "lastIndexOf" | "localeCompare" | ... 36 more ... | "valueOf"
type KeyOfNumber = keyof 42
// ="toFixed" | "toExponential" | "toPrecision" | "toLocaleString" | "toString" | "valueOf"
ما میتوانیم ببینیم که keyof وقتی روی مقادیر نوع غیر رکورد اعمال میشود، اعضای پروتوتایپهای مربوطه آنها را برمیگرداند
راهحل (خلاصه)
یکی از راههایی که میتوانید مشکل را حل کنید این است که پارامتر نوع دوم در Foo<K, T> را حذف کنید و یک نوع مشخص برای اشاره هنگام محدود کردن K ارائه دهید، به عنوان مثال.
interface MyConcreteTypeDef {
}
type Foo<K> = K extends keyof MyConcreteTypeDef ? MyConcreteTypeDef[K] : never;
function test1<K>(foo: Foo<K>) { }
function test2<K extends keyof MyConcreteTypeDef>(foo: MyConcreteTypeDef[K]) {
test1(foo);
}
پاسخ شماره ۲
این ممکن است کمک کند، به نظر میرسد در راستای چیزی است که شما سعی در دستیابی به آن دارید. استفاده از تایپاسکریپت برای بهبود تجربه توسعهدهنده در یک زمینه برنامهنویسی شیءگرا وقتی که موضوع دستکاری اشیاء است، در اینجا لینک محیط تمرینی است (ماژول را در تنظیمات tsconfig روی ESNext قرار دهید -- متد Object.fromEntries قبل از ES2018 وجود ندارد)
type Unenumerate<T> = T extends Array<infer U>
? U
: T extends ReadonlyArray<infer U>
? U
: T;
// a stricter implementation of TypeScripts Omit helper
type RemoveFields<T, P extends keyof T = keyof T> = {
[S in keyof T as Exclude<S, P>]: T[S];
};
type UnknownObjOrEnum =
| Record<string | number | symbol, unknown>
| Enumerator<unknown>;
function extractTupleHelper<
const T extends UnknownObjOrEnum,
const V extends keyof T
>(obj: T, props: V) {
return [props, obj[props]] as const satisfies readonly [V, T[V]];
}
function includeTargeted<
const T extends UnknownObjOrEnum,
const V extends keyof T
>({ obj, targetedKeys }: { obj: T; targetedKeys: V[] }): Pick<T, V> {
return Object.fromEntries(
targetedKeys.map(t => extractTupleHelper(obj, t))
) as Pick<T, Unenumerate<typeof targetedKeys>>;
}
function excludeTargeted<
const T extends UnknownObjOrEnum,
const V extends keyof T
>({ obj, targetedKeys }: { obj: T; targetedKeys: V[] }): RemoveFields<T, V> {
return Object.fromEntries(
Object.entries(obj)
.map(([key, val]) => {
if (targetedKeys.includes(key as V)) {
return ["omit", "omit"] as const;
} else return [key, val] as const;
})
.filter(([t, _]) => /omit/.test(t) === false)
) as RemoveFields<T, Unenumerate<typeof targetedKeys>>;
}
const testObj = {
some: "strongly",
typed: "object",
can: "be",
filtered: "by",
anArray: "of",
keys: "to",
include: "or",
exclude: "the",
targeted: "keys"
} as const;
enum TestEnum {
THIS = "also",
EXTENDS = "to",
ENUMERATORS = "by",
INCLUDING = "ScriptHost",
IN = "the",
LIB_ARRAY = "of",
YOUR = "tsconfig.json"
}
const excludeTest = () => excludeTargeted({
obj: testObj,
targetedKeys: ["exclude", "filtered", "keys"]
});
const includeTest = () => includeTargeted({
obj: testObj,
targetedKeys: ["include", "filtered", "keys"]
});
const excludeTestEnum = () => excludeTargeted({
obj: TestEnum,
targetedKeys: ["INCLUDING", "ENUMERATORS"]
});
const includeTestEnum = () => includeTargeted({
obj: TestEnum,
targetedKeys: ["INCLUDING", "ENUMERATORS"]
});
این هندلرهای شیء و شمارنده انعطافپذیر برای هر مورد استفادهای کار میکنند و اینتللیسنس بهطور خودکار به شامل یا حذف بودن اشاره میکند همانطور که در دو تصویر زیر نشان داده شده است

