التحويل - TypeScript: توقيع الفهرس مفقود في النوع
أريد أن يكون MyInterface.dic مثل قاموس name: value، وأقوم بتعريفه كما يلي:
interface MyInterface {
dic: { [name: string]: number }
}
الآن أنشئ دالة تنتظر نوعي:
function foo(a: MyInterface) {
...
}
والإدخال:
let o = {
dic: {
'a': 3,
'b': 5
}
}
أتوقع أن يكون foo(o) صحيحًا، لكن المترجم ينهار:
foo(o) // TypeScript error: Index signature is missing in type { 'a': number, 'b': number }
أعلم أن هناك تحويلًا ممكنًا: let o: MyInterface={ ... } الذي يؤدى الغرض، لكن لماذا لا يتعرف TypeScript على نوعي؟
إضافي: يعمل بشكل جيد إذا تم إعلان o ضمن السطر:
foo({
dic: {
'a': 3,
'b': 5
}
})
الإجابة رقم 1
المشكلة هي أنه عندما يتم استنتاج النوع، فإن نوع o يكون:
{ dic: { a: number, b: number } }
هذا ليس هو نفسه { dic: { [name: string]: number } }. والأهم من ذلك، مع التوقيع الأول غير مسموح لك بفعل شيء مثل o.dic['x']=1. مع التوقيع الثاني، يمكنك ذلك.
هم أنواع متكافئة في وقت التشغيل (في الواقع، هم نفس القيمة بالضبط)، لكن جزءًا كبيرًا من أمان TypeScript يأتي من حقيقة أن هذه ليست هي نفسها، وأنه سيسمح لك فقط بالتعامل مع كائن كمعجم إذا كان يعرف أنه مقصود صراحةً بذلك. هذا ما يمنعك من قراءة وكتابة خصائص غير موجودة تمامًا على الكائنات عن طريق الخطأ.
الحل هو التأكد من أن TypeScript يعرف أنه مقصود كمفردات. وهذا يعني:
تحديد نوع صراحة في مكان ما يخبره أنه قاموس:
let o: MyInterfaceتأكيد أنه قاموس ضمن السطر:
let o = { dic: <{ [name: string]: number }> { 'a': 1, 'b': 2 } }ضمان أنه النوع الأولي الذي يستنتجه TypeScript لك:
foo({ dic: { 'a': 1, 'b': 2 } })
إذا كانت هناك حالة يعتقد فيها TypeScript أنه كائن عادي يحتوي فقط على خاصيتين، ثم تحاول استخدامه لاحقًا كقاموس، فسوف يكون غير سعيد.
الإجابة رقم 2
في حالتي، كان من الضروري فقط استخدام type بدلًا من interface.
الإجابة رقم 3
يريد TS منا تعريف نوع المؤشر. على سبيل المثال، لإخبار المترجم أنه يمكنك فهرسة الكائن بأي نص، مثل myObj['anyString']، قم بتغيير:
interface MyInterface {
myVal: string;
}
إلى :
interface MyInterface {
[key: string]: string;
myVal: string;
}
ويمكنك الآن تخزين أي قيمة نصية في أي فهرس نصي:
x['myVal'] = 'hello world'
x['any other string'] = 'any other string'
رابط الملعب.
الإجابة رقم 4
بالنسبة لي، تم حل الخطأ باستخدام type بدلًا من interface.
يمكن أن يحدث هذا الخطأ عندما تحتوي الدالة foo على type بدلاً من interface لمعامل الكتابة، مثل:
type MyType = {
dic: { [name: string]: number }
}
function foo(a: MyType) {}
لكن القيمة الممررة مكتوبة باستخدام واجهة مثل
interface MyInterface {
dic: { [name: string]: number }
}
const o: MyInterface = {
dic: {
'a': 3,
'b': 5
}
}
foo(o) // type error here
لقد استخدمت للتو
const o: MyType = {
dic: {
'a': 3,
'b': 5
}
}
foo(o) // It works
الإجابة #5
يمكنك حل هذه المشكلة عن طريق القيام بـ foo({...o}) في ساحة اللعب
الإجابة رقم 6
إليكم رأيي:
type Copy<T> = { [K in keyof T]: T[K] }
genericFunc<SomeType>() // No index signature
genericFunc<Copy<SomeType>>() // No error
الإجابة رقم 7
المشكلة أوسع قليلاً من سؤال صاحب الموضوع.
على سبيل المثال، دعنا نعرف واجهة ومتغير للواجهة
interface IObj {
prop: string;
}
const obj: IObj = { prop: 'string' };
هل يمكننا تعيين obj إلى النوع Record<string, string>؟
الإجابة هي لا. تجربة
// TS2322: Type 'IObj' is not assignable to type 'Record<string, string>'. Index signature for type 'string' is missing in type 'IObj'.
const record: Record<string, string> = obj;
لماذا يحدث هذا؟ لوصف ذلك، لنُجدّد فهمنا لمصطلحي "التحويل التصاعدي" و"التحويل التنازلي", وما معنى حرف "L" في مبادئ SOLID.
الأمثلة التالية تعمل بدون أخطاء لأننا نخصص النوع "الأوسع" للنمط الأكثر صرامة.
تجريبي
const initialObj = {
title: 'title',
value: 42,
};
interface IObj {
title: string;
}
const obj: IObj = initialObj; // No error here
obj.title;
obj.value; // Property 'value' does not exist on type 'IObj'.(2339)
يتطلب IObj خاصية واحدة فقط لذا فإن التعيين صحيح.
يعمل نفس الشيء بالنسبة لنوع. عرض توضيحي
const initialObj = {
title: 'title',
value: 42,
};
type TObj = {
title: string;
}
const obj: TObj = initialObj; // No error here
obj.title;
obj.value; // Property 'value' does not exist on type 'TObj'.(2339)
تعمل المثالا الأخيرة دون أخطاء بسبب "الترقية النوعية" (upcasting). هذا يعني أننا نحول نوع القيمة إلى النوع "الأعلى"، إلى النوع الكيان الذي يمكن أن يكون سلفًا. بعبارة أخرى، يمكننا تعيين Dog إلى Animal ولكن لا يمكننا تعيين Animal إلى Dog (انظر معنى الحرف "L" في مبادئ SOLID). تعيين الكلب إلى الحيوان هو "ترقية نوعية" وهذه عملية آمنة.
Record<string, string> أوسع بكثير من الكائن الذي يحتوي على خاصية واحدة فقط. يمكن أن يحتوي على أي خصائص أخرى.
const fn = (record: Record<string, string>) => {
record.value1;
record.value2;
record.value3; // No errors here
}
لهذا السبب عندما تقوم بتعيين واجهة IObj إلى Record<string, string> تحصل على خطأ. تقوم بتعيينها إلى النوع الذي يمدد IObj. يمكن أن يكون نوع Record<string, string> تابعًا لـ IObj.
في إجابات أخرى، ذُكر أن استخدام Type يمكن أن يحل المشكلة. لكني أعتقد أن هذا سلوك خاطئ ويجب علينا تجنب استخدامه.
مثال :
type TObj = {
title: string;
}
const obj: TObj = {
title: 'title',
};
const fn = (record: Record<string, string>) => {
record.value1;
record.value2;
// No errors here because according to types any string property is correct
// UPD:
// FYI: TS has a flag `noUncheckedIndexedAccess` which changes this behavior so every prop becomes optional
record.value3;
}
fn(obj); // No error here but it has to be here because of downcasting
المثال الأخير مع مقارنة النوع والواجهة.
ملاحظة
ألقِ نظرة على هذه المشكلة مع السؤال ذي الصلة، والتعليق المثير للاهتمام.
