Typescript: اقتراح: اكتب نوع الخاصية

تم إنشاؤها على ٢٨ نوفمبر ٢٠١٤  ·  76تعليقات  ·  مصدر: microsoft/TypeScript

الدوافع

تتضمن الكثير من مكتبة / إطار عمل / نمط جافا سكريبت حسابًا يعتمد على اسم خاصية الكائن. على سبيل المثال ، نموذج العمود الفقري ، التحول الوظيفي pluck ، ImmutableJS كلها مبنية على مثل هذه الآلية.

//backbone
var Contact = Backbone.Model.extend({})
var contact = new Contact();
contact.get('name');
contact.set('age', 21);

// ImmutableJS
var map = Immutable.Map({ name: 'François', age: 20 });
map = map.set('age', 21);
map.get('age'); // 21

//pluck
var arr = [{ name: 'François' }, { name: 'Fabien' }];
_.pluck(arr, 'name') // ['François', 'Fabien'];

يمكننا أن نفهم بسهولة في هذه الأمثلة العلاقة بين API وقيد النوع الأساسي.
في حالة نموذج العمود الفقري ، فهو مجرد نوع من _proxy_ لكائن من النوع:

interface Contact {
  name: string;
  age: number;
}

بالنسبة لحالة pluck ، فهو تحول

T[] => U[]

حيث U هو نوع خاصية T prop .

ومع ذلك ، ليس لدينا طريقة للتعبير عن مثل هذه العلاقة في TypeScript ، وينتهي الأمر بكتابة ديناميكية.

الحل المقترح

الحل المقترح هو تقديم بناء جملة جديد لنوع T[prop] حيث prop هو وسيطة للدالة باستخدام نوع مثل قيمة الإرجاع أو معلمة النوع.
باستخدام بناء الجملة الجديد هذا ، يمكننا كتابة التعريف التالي:

declare module Backbone {

  class Model<T> {
    get(prop: string): T[prop];
    set(prop: string, value: T[prop]): void;
  }
}

declare module ImmutableJS {
  class Map<T> {
    get(prop: string): T[prop];
    set(prop: string, value: T[prop]): Map<T>;
  }
}

declare function pluck<T>(arr: T[], prop: string): Array<T[prop]>  // or T[prop][] 

بهذه الطريقة ، عندما نستخدم نموذج العمود الفقري الخاص بنا ، يمكن لـ TypeScript كتابة التحقق من المكالمة get و set بشكل صحيح.

interface Contact {
  name: string;
  age: number;
}
var contact: Backbone.Model<Contact>;

var age = contact.get('age');
contact.set('name', 3) /// error

ثابت prop

قيد

من الواضح أن الثابت يجب أن يكون من النوع الذي يمكن استخدامه كنوع فهرس ( string ، number ، Symbol ).

حالة قابلة للفهرسة

دعونا نلقي نظرة على تعريفنا Map :

declare module ImmutableJS {
  class Map<T> {
    get(prop: string): T[string];
    set(prop: string, value: T[string]): Map<T>;
  }
}

إذا كان T قابلاً للفهرسة ، فإن خريطتنا ترث هذا السلوك:

var map = new ImmutableJS.Map<{ [index: string]: number}>;

الآن get له النوع get(prop: string): number .

استجواب

الآن هناك بعض الحالات التي أشعر فيها بالألم عند التفكير في سلوك _ صحيح _ ، فلنبدأ مرة أخرى بتعريفنا Map .
إذا بدلاً من تمرير { [index: string]: number } كمعامل نوع كنا سنقدم
{ [index: number]: number } يجب أن يقوم المترجم بإصدار خطأ؟

إذا استخدمنا pluck مع تعبير ديناميكي للخاصية بدلاً من الثابت:

var contactArray: Contact[] = []
function pluckContactArray(prop: string) {
  return _.pluck(myArray, prop);
}

أو مع ثابت ليس خاصية من النوع الذي تم تمريره كمعامل.
يجب أن يؤدي استدعاء pluck حدوث خطأ لأن المترجم لا يمكنه استنتاج النوع T[prop] ، shoud T[prop] حله إلى {} أو any إذا كان الأمر كذلك ، فهل يجب أن يقوم المترجم الذي يحتوي على --noImplicitAny بإصدار خطأ؟

Fixed Suggestion help wanted

التعليق الأكثر فائدة

weswighammhegazy، ولقد تم مناقشة هذا مؤخرا. سنخبرك بأي تطورات نواجهها ، وتذكر أن هذا مجرد نموذج أولي للفكرة.

الأفكار الحالية:

  • عامل تشغيل keysof Foo ينتزع اتحاد أسماء الخصائص من Foo كأنواع حرفية للسلسلة.
  • نوع Foo[K] الذي يحدد أنه بالنسبة لنوع ما K يكون هذا النوع عبارة عن أنواع حرفية للسلسلة أو اتحاد أنواع حرفية للسلسلة.

من هذه الكتل الأساسية ، إذا كنت بحاجة إلى استنتاج سلسلة حرفية كنوع مناسب ، يمكنك الكتابة

function foo<T, K extends keysof T>(obj: T, key: K): T[K] {
    // ...
}

هنا ، سيكون K نوعًا فرعيًا من keysof T مما يعني أنه نوع سلسلة حرفية أو اتحاد أنواع حرفية للسلسلة. أي شيء تقوم بتمريره للمعلمة key يجب كتابته حسب السياق من خلال ذلك الحرفي / اتحاد القيم الحرفية ، واستنتاجه كنوع حرفي لسلسلة فردية.

على سبيل المثال

interface HelloWorld { hello: any; world: any; }

function foo<K extends keysof HelloWorld>(key: K): K {
    return key;
}

// 'x' has type '"hello"'
let x = foo("hello");

أكبر مشكلة هي أن keysof يحتاج غالبًا إلى "تأخير" تشغيله. إنه حريص جدًا على كيفية تقييمه لنوع ما ، وهي مشكلة بالنسبة لمعلمات النوع كما في المثال الأول الذي نشرته (على سبيل المثال ، الحالة التي نريد حلها هي في الواقع الجزء الصعب: الابتسامة :).

اتمنى ان يعطيك كل تحديث

ال 76 كومينتر

ممكن تكرار رقم 394

راجع أيضًا https://github.com/Microsoft/TypeScript/issues/1003#issuecomment -61171048

NoelAbrahams لا أعتقد حقًا أنها نسخة مكررة من # 394 ، بل على العكس من ذلك ، فإن كلتا الميزتين مكملتان تمامًا مثل:

 class Model<T> {
    get(prop: memberof T): T[prop];
    set(prop:  memberof T, value: T[prop]): void;
  }

سيكون مثاليا

تضمين التغريدة

contact.set(Math.random() >= 0.5 ? 'age' : 'name', 13)

ماذا تفعل في هذه الحالة؟

إنها نفس الحالة أكثر أو أقل من تلك الموجودة في الفقرة الأخيرة من مشكلتي. كما قلت ، لدينا خيارات متعددة ، يمكننا الإبلاغ عن خطأ ، أو استنتاج any مقابل T[prop] ، أعتقد أن الحل الثاني أكثر منطقية

اقتراح رائع. موافق ، ستكون ميزة مفيدة.

fdecampredon ، أعتقد أن هذه نسخة مكررة. شاهد تعليق Dan والرد المقابل الذي يحتوي على اقتراح membertypeof .

كل هذا في IMO عبارة عن الكثير من البنية الجديدة لحالة استخدام ضيقة إلى حد ما.

NoelAbrahams ليس نفس الشيء.

  • يُرجع memberof T النوع الذي يمكن أن يكون المثال عبارة عن سلسلة ذات اسم خاصية صالح لـ T مثيل.
  • T[prop] نوع الخاصية T المسماة بسلسلة يتم تمثيلها بواسطة وسيطة / متغير prop .

هناك brifge لـ memberof هذا النوع من المعلمة prop يجب أن يكون memberof T .

في الواقع ، أود أن يكون لدي نظام أكثر ثراءً لاستدلال النوع بناءً على نوع البيانات الوصفية. لكن هذا المشغل هو بداية جيدة وكذلك memberof .

هذا مثير للاهتمام ومرغوب فيه. لا يعمل TypeScript جيدًا مع الأطر ذات السلسلة الثقيلة حتى الآن ، ومن الواضح أن هذا سيساعد كثيرًا.

لا يعمل TypeScript بشكل جيد مع الأطر الثقيلة

صحيح. لا يزال لا يغير حقيقة أن هذا اقتراح مكرر.

حتى الآن ومن الواضح أن هذا من شأنه أن يساعد كثيرًا [في كتابة الأطر ذات السلسلة الثقيلة]

لست متاكد حيال ذلك. يبدو مجزأ إلى حد ما ومخصص إلى حد ما لنمط الكائن الوكيل الموضح أعلاه. أفضل بكثير اتباع نهج أكثر شمولاً لمشكلة السلسلة السحرية على غرار # 1003.

يقترح 1003 any كنوع الإرجاع للحاصل. يضيف هذا الاقتراح في الأعلى أنه من خلال إضافة طريقة للبحث عن نوع القيمة أيضًا - سيؤدي المزيج إلى شيء مثل هذا:

declare module ImmutableJS {
  class Map<T> {
    get(prop: memberof T): T[prop];
    set(prop: memberof T, value: T[prop]): Map<T>;
  }
}

spion ، هل تقصد # 394؟ إذا كنت ستقرأ المزيد ، فسترى ما يلي:

فكرت في نوع الإرجاع لكنني تركته حتى لا أجعل الاقتراح العام كبيرًا جدًا.

كان هذا فكرتي الأولية ولكن لديه مشاكل. ماذا لو كانت هناك وسيطات متعددة من النوع memberof T ، أي منها يشير إليها membertypeof T ؟

get(property: memberof T): membertypeof T;
set(property: memberof T, value: membertypeof T);

هذا يحل مشكلة "أي حجة أشير إليها" ، لكن اسم membertypeof يبدو خاطئًا وليس من محبي عامل التشغيل الذي يستهدف اسم الخاصية.

get(property: memberof T): membertypeof property;
set(property: memberof T, value: membertypeof property);

أعتقد أن هذا يعمل بشكل أفضل.

get(property: memberof T is A): A;
set(property: memberof T is A, value: A)

لسوء الحظ ، لست متأكدًا من أن لدي حلاً رائعًا على الرغم من أنني أعتقد أن الاقتراح الأخير لديه إمكانات جيدة.

حسنًا ، NoelAbrahams كان هناك تعليق في # 394 كان يحاول أن يصف أكثر أو أقل نفس الشيء الذي هذا التعليق.
أعتقد الآن أن أكثر من T[prop] ربما يكون أكثر أناقة قليلاً من الاقتراحات المختلفة لهذا التعليق ، وأن الاقتراح في هذه القضية ربما يذهب أبعد قليلاً في التفكير.
لهذا السبب ، لا أعتقد أنه يجب إغلاقها كنسخة مكررة.
لكني أعتقد أنني متحيز لأنني من كتب القضية ؛).

fdecampredon ، أكثر مرحا: مبتسم:

NoelAbrahams عفوا ، فاتني هذا الجزء. بالتأكيد ، هذه متكافئة إلى حد كبير (لا يبدو أن هذه المعلمة تقدم معلمة عامة أخرى ، والتي قد تكون مشكلة أو لا تكون كذلك)

بعد إلقاء نظرة على Flow ، أعتقد أنه سيكون أكثر أناقة مع نظام كتابة أقوى قليلاً وأنواع خاصة بدلاً من تضييق نوع مخصص.

ما نعنيه بـ get(prop: string): Contact[prop] على سبيل المثال هو مجرد سلسلة من التحميل الزائد المحتمل:

interface Map {
  get(prop : string) : Contact[prop];
}

// is morally equivalent to 

interface Map {
  get(prop : "name") : string;
  get(prop : "age") : number;
}

بافتراض وجود عامل التشغيل من النوع & (أنواع التقاطع) ، فهذا النوع يعادل

interface Map {
   get : (prop : "name") => string & (prop : "age") => number;
}

الآن بعد أن قمنا بترجمة حالتنا غير العامة في تعبيرات من الأنواع فقط بدون معاملة خاصة (بدون [prop] ) ، يمكننا معالجة مسألة المعلمات.

الفكرة هي إنشاء هذا النوع إلى حد ما من معلمة نوع. قد نحدد بعض الأنواع العامة الوهمية الخاصة $MapProperties ، $Name و $Value للتعبير عن النوع الذي تم إنشاؤه من حيث تعبير النوع فقط (بدون بناء جملة خاص) مع الاستمرار في التلميح إلى مدقق النوع أن شيئا ما ينبغي القيام به.

class Map<T> {
   get : $MapProperties<T, (prop : $Name) => $Value>
   set : $MapProperties<T, (prop : $Name, val : $Value) => void>
}

قد يبدو الأمر معقدًا وقريبًا من النماذج أو أنواعًا تعتمد على الرجل الفقير ولكن لا يمكن تجنبه عندما يريد شخص ما أن تعتمد الأنواع على القيم.

هناك مجال آخر حيث يكون هذا مفيدًا وهو التكرار على خصائص كائن مكتوب:

interface Env {
 // pretend this is an actually interesting type
};

var actions = {
  action1: function (env: Env, x: number) : void {},
  action2: function (env: Env, y: string) : void {}
};

// actions has type { action1: (Env, number) => void; action2: (Env, string) => void; }
var env : Env = {};
var boundActions = {};
for (var action in actions) {
  boundActions[action] = actions[action].bind(null, env);
}

// boundActions should have type { action1: (number) => void; action2: (string) => void; }

يجب أن تكون هذه الأنواع ممكنة نظريًا على الأقل للاستدلال (توجد معلومات كافية عن النوع لاستنتاج نتيجة حلقة for ) ، لكنها أيضًا على الأرجح امتداد تمامًا.

لاحظ أن الإصدار التالي من رد الفعل سيستفيد بشكل كبير من هذا النهج ، راجع https://github.com/facebook/react/issues/3398

مثل https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -64944856 ، عندما يتم توفير السلسلة بواسطة تعبير آخر بخلاف سلسلة حرفية ، تنهار هذه الميزة بسرعة بسبب مشكلة التوقف. ومع ذلك ، هل يمكن تنفيذ إصدار أساسي من هذا باستخدام التعلم من مشكلة رمز ES6 (# 2012)؟

وافق.

سنرغب في تجربة ذلك في فرع تجريبي للتعرف على التركيب اللغوي.

أتساءل فقط أي إصدار من الاقتراح سيتم تنفيذه؟ أي أنه سيتم تقييم T[prop] في موقع الاتصال واستبداله بشغف بنوع ملموس أم أنه سيصبح شكلاً جديدًا من متغير النوع؟

أعتقد أننا يجب أن نعتمد على بناء جملة أكثر عمومية وأقل إسهابًا كما هو محدد في # 3779.

interface Map<T> {
  get<A>(prop: $Member<T,A>): A;
  set<A>(prop: $Member<T,A>, value: A): Map<T>;
}

أم أنه لا يمكن استنتاج نوع A؟

أريد فقط أن أقول إنني صنعت أداة ترميز صغيرة لجعل تكامل TS مع ImmutableJS أسهل أثناء انتظار الحل العادي: https://www.npmjs.com/package/tsimmutable. إنه بسيط للغاية ، لكنني أعتقد أنه سيعمل مع معظم حالات الاستخدام. ربما سيساعد شخص ما.

أريد أيضًا أن أشير إلى أن الحل مع نوع العضو قد لا يعمل مع ImmutableJS:

interface Profile {
  firstName: string 
}

interface User {
  profile: Profile  
}

let a: Map<User> = fromJS(/* ... */);
a.get('profile') // Type will be Profile, but the real type is Map<Profile>!

@ s-panferov شيء من هذا القبيل يمكن أن يعمل:

interface ImmutableMap<T> {
    get<A extends boolean | number | string>(key : string) : A;
    get<A extends {}>(key : string) : ImmutableMap<A>;
    get<E, A extends Array<any>>(key : string) : ImmutableList<E>;
}

interface Profile {

}

interface User {
    name : string;
    profile : Profile;
}

var map : ImmutableMap<User>;

var name = map.get<string>('name'); // string
var profile = map.get<Profile>('profile'); // ImmutableMap<Profile>

لن يستثني هذا عُقد DOM أو كائنات التاريخ ولكن لا ينبغي السماح باستخدامها في الهياكل الثابتة على الإطلاق. https://github.com/facebook/immutable-js/wiki/Converting-from-JS-objects

العمل تدريجيًا من أفضل حل بديل

أعتقد أنه من المفيد البدء أولاً بأفضل حل بديل متاح حاليًا ، ثم العمل عليه تدريجيًا من هناك.

لنفترض أننا بحاجة إلى دالة تُرجع قيمة خاصية لأي كائن غير بدائي يحتوي

function getProperty<T extends object>(container: T; propertyName: string) {
    return container[propertyName];
}

نريد الآن أن يكون لقيمة الإرجاع نوع الخاصية الهدف ، لذلك يمكن إضافة معلمة نوع عام أخرى لنوعها:

function getProperty<T extends object, P>(container: T; propertyName: string) {
    return <P> container[propertyName];
}

لذلك ستبدو حالة الاستخدام مع الفصل كما يلي:

class C {
    member: number;
    static member: string;
}

let instance = new C();
let result = getProperty<C, typeof instance.member>(instance, "member");

وسيتلقى result النوع number بشكل صحيح.

ومع ذلك ، يبدو أن هناك مرجعًا مكررًا لـ "عضو" في الاستدعاء: أحدهما في معلمة النوع ، والآخر عبارة عن سلسلة _literal_ ستتلقى دائمًا تمثيل سلسلة لاسم الخاصية الهدف. تتمثل فكرة هذا الاقتراح في أنه يمكن توحيد هذين النوعين في معلمة من نوع واحد لا تتلقى سوى تمثيل السلسلة.

لذلك يجب أن نلاحظ هنا أن السلسلة ستعمل أيضًا كمعامل _عام_ ، وستحتاج إلى تمريرها كمعامل حرفي لكي يعمل هذا (يمكن للحالات الأخرى تجاهل قيمتها بصمت ما لم يتوفر شكل من أشكال انعكاس وقت التشغيل). لتوضيح الدلالات ، يجب أن تكون هناك طريقة ما للدلالة على العلاقة بين المعلمة العامة والسلسلة:

function getProperty<T extends object, PName: string = propertyName>(container: T; propertyName: string) {
    return <T[PName]> container[propertyName];
}

والآن ستبدو المكالمات كما يلي:

let instance = new C();
let result = getProperty<C>(instance, "member");

والذي من شأنه أن يقرر داخليًا:

let result = getProperty<C, "member">(instance, "member");

ومع ذلك ، نظرًا لأن C يحتوي على كل من مثيل وخاصية ثابتة تسمى member ، فإن التعبير العام من النوع الأعلى T[PName] غامض. نظرًا لأن النية هنا هي في الغالب لتطبيقها على خصائص المثيلات ، يمكن استخدام حل مثل عامل التشغيل المقترح T[PName] قبل البعض على أنه يمثل مرجع _value_ ، وليس بالضرورة نوعًا:

function getProperty<T extends object, PName: string = propertyName>(container: T; propertyName: string) {
    return <typeon T[PName]> container[propertyName];
}

(قد يعمل هذا أيضًا إذا كان T هو نوع واجهة متوافق ، حيث أن typeon يدعم الواجهات أيضًا)

للحصول على الخاصية الثابتة ، سيتم استدعاء الدالة مع المُنشئ نفسه ويجب تمرير نوع المُنشئ كـ typeof C :

let result = getProperty<typeof C>(C, "member"); // Note it is called with the constructor object

(داخليًا يتحول typeon (typeof C) إلى typeof C لذلك سيعمل هذا)
سيتلقى الآن result النوع string بشكل صحيح.

لدعم أنواع إضافية من معرفات الخاصية ، يمكن إعادة كتابة هذا على النحو التالي:

type PropertyIdentifier = string|number|Symbol;

function getProperty<T extends object, PName: PropertyIdentifier = propertyName>(container: T; propertyName: PropertyIdentifier) {
    return <typeon T[PName]> container[propertyName];
}

لذلك هناك العديد من الميزات المختلفة اللازمة لدعم هذا:

  • السماح بتمرير القيم الحرفية كمعلمات عامة.
  • اسمح لتلك القيم الحرفية بتلقي قيمة وسيطات الدالة.
  • السماح بنوع من الأدوية عالية الجودة.
  • السماح بالإشارة إلى نوع خاصية من خلال معلمة عامة حرفية على نوع _generic_ من خلال الأنواع الأعلى من النوع.

إعادة التفكير في المشكلة

الآن دعنا نعود إلى الحل الأصلي:

function getProperty<T extends object, P>(container: T; propertyName: string) {
    return <P> container[propertyName];
}

هناك ميزة واحدة حول هذا الحل البديل ، وهي أنه يمكن توسيعه بسهولة لدعم أسماء مسارات أكثر تعقيدًا ، مع الإشارة ليس فقط إلى الخصائص المباشرة ، ولكن إلى خصائص الكائنات المتداخلة:

function getPath<T extends object, P>(container: T; path: string) {
    ... more complex code here ...

    return <P> resultValue;
}

و الأن:

class C {
    a: {
        b: {
            c: string[];
        }
    }
}
let instance = new C();
let result = getPath<C, typeof instance.a.b.c>(instance, "a.b.c");

سيحصل result بشكل صحيح على النوع string[] هنا.

السؤال هو ، هل يجب دعم المسارات المتداخلة أيضًا؟ هل ستكون الميزة مفيدة حقًا إذا لم يتوفر الإكمال التلقائي عند كتابتها؟

يشير هذا إلى أنه قد يكون من الممكن إيجاد حل مختلف ، والذي سيعمل في الاتجاه الآخر. العودة إلى الحل الأصلي:

function getProperty<T extends object, P>(container: T; propertyName: string) {
    return <P> container[propertyName];
}

بالنظر إلى هذا من الاتجاه الآخر ، من الممكن أخذ مرجع النوع نفسه وتحويله إلى سلسلة نصية:

function getProperty<T extends object, P>(container: T; propertyName: string = @typeReferencePathOf(P)) {
    return <P> container[propertyName];
}

@typeReferencePathOf مشابه في المفهوم لـ nameOf لكنه ينطبق على المعلمات العامة. قد يتطلب الأمر مرجع النوع المستلم typeof instance.member (أو بدلاً من ذلك typeon C.member ) ، واستخراج مسار الخاصية member ، وتحويله إلى السلسلة الحرفية "member" عند التجميع زمن. قد يتم حل @typeReferenceOf التكميلي الأقل تخصصًا إلى السلسلة الكاملة "typeof instance.member" .

و الآن:

getProperty<C, typeof instance.subObject>(instance);

نقرر:

getProperty<C, typeof instance.subObject>(instance, "subObject");

وتنفيذ مماثل لـ getPath :

getPath<C, typeof instance.a.b.c>(instance);

نقرر:

getPath<C, typeof instance.a.b.c>(instance, "a.b.c");

نظرًا لأن @typeReferencePathOf(P) يتم تعيينه كقيمة افتراضية فقط. يمكن ذكر الحجة يدويًا إذا لزم الأمر:

getPath<C, SomeTypeWhichIsNotAPath>(instance, "member.someSubMember.AnotherSubmember.data");

إذا لم يتم توفير معلمة النوع الثاني ، فقد يتم حل المعلمة @typeReferencePathOf() إلى undefined أو بدلاً من ذلك السلسلة الفارغة "" . إذا تم توفير نوع ولكن لم يكن له مسار داخلي ، فسيتم حله إلى "" .

مزايا هذا النهج:

  • لا يتطلب أدوية من النوع العالي.
  • لا يتطلب السماح بالحروف كمعلمات عامة.
  • لا يتطلب تمديد الأدوية بأي شكل من الأشكال.
  • يسمح بالإكمال التلقائي عند كتابة التعبير typeof ويستخدم آلية موجودة للتحقق من النوع للتحقق من صحته بدلاً من الحاجة إلى تحويله من تمثيل سلسلة في وقت الترجمة.
  • يمكن استخدامها ضمن الفئات العامة أيضًا.
  • لا يتطلب typeon (لكن يمكنه دعمه).
  • يمكن تطبيقها في سيناريوهات أخرى.

+1

لطيف! لقد بدأت بالفعل في كتابة اقتراح كبير لمعالجة نفس المشكلة ، ولكن وجدت هذه المناقشة ، لذلك دعونا نناقش هنا.

فيما يلي مقتطف من مشكلتي التي لم يتم إرسالها:

سؤال : كيف يجب التصريح عن الوظيفة التالية في d.ts لتكون قابلة للاستخدام في السياقات التي من المفترض استخدامها فيها؟

function mapValues(obj, fn) {
      return Object.keys(obj)
          .map(key => ({key, value: fn(obj[key], key)}))
          .reduce((res, {key, value}) => (res[key] = value, res), {})
}

يمكن العثور على هذه الوظيفة (مع اختلافات طفيفة) في كل مكتبة "أدوات" عامة تقريبًا .

هناك حالتا استخدام مميزتان لـ mapValues في البرية:

أ. _Object-as-a -__ قاموس _ حيث تكون أسماء الخصائص ديناميكية ، والقيم لها نفس النوع ، وتكون الوظيفة في الحالة العامة

var obj = {'a': 1, 'b': 2, 'c': 3};
var res = mapValues(x, val => val * 5); // {a: 5, b: 10, c: 15}
console.log(res['a']) // 5

ب. _Object-as-a- _Record _ حيث يكون لكل خاصية نوع خاص بها ، بينما تكون الوظيفة متعددة الأشكال حدوديًا (عن طريق التنفيذ)

var obj = {a: 123, b: "Hello", c: true};
var res = mapValues(p, val => [val]); // {a: [123], b: ["Hello"], c: [true]}
console.log(res.a[0].toFixed(2)) // "123.00"

أفضل أداة متاحة لـ mapValues مع TypeScript الحالية هي:

declare function mapValues<T1, T2>(
    obj: {[key: string]: T1},
    fn: (arg: T1, key: string) => T2
): {[key: string]: T2};

ليس من الصعب رؤية أن هذا التوقيع يتطابق تمامًا مع حالة استخدام _Object-as-a _Dictionary _ ، بينما ينتج نتيجة واحدة مفاجئة إلى حد ما لاستدلال النوع عند تطبيقه في الحالة التي يكون فيها _Object-as-a-_ _Record _ كان القصد.

سيتم إجبار النوع {p1: T1, p2: T2, ...pn: Tn} على {[key: string]: T1 | T2 | ... | Tn } يخلط بين جميع الأنواع ويتجاهل بشكل فعال جميع المعلومات حول بنية السجل:

  • سوف يرفض المترجم القيمة الصحيحة والمتوقعة 2 console.log(res.a[0].toFixed(2)) ؛
  • يحتوي console.log((<number>res['a'][0]).toFixed(2)) المقبول على مكانين غير خاضعين للرقابة ومعرضين للخطأ: اسم الخاصية type-cast وخاصية عشوائية.

1 ، 2 - للمبرمجين الذين يهاجرون من ES إلى TS


الخطوات الممكنة لمعالجة هذه المشكلة (وحل المشكلات الأخرى أيضًا)

1. إدخال السجلات المتنوعة بمساعدة أنواع السلاسل الحرفية

type Numbers<p extends string> =  { ...p: number };
type NumbersOpt<p extends string> = {...p?: number };
type ABC = "a" | "b" | "c";
type abc = Numbers<ABC> // abc =  {a: number, b: number, c: number} 
type abcOpt = NumbersOpt<ABC> // abcOpt =  {a?: number, b?:number, c?: number}

function toFixedAll<p extends string>(obj: {...p: number}, precision):{...p: string}  {
      var result: {...p: string} = {} as any;
      Object.keys(obj).forEach((p:p) => {
           result[p] = obj[p].toFixed(precision);
      });
      return result;
}

var test = toFixedAll({x:5, y:7}, 3); // { x: "5.00", y: "6.00" },  p inferred as "x"|"y"
console.log(test.y.length) // 4   test.y: string
2. السماح بتدوين الأنواع الرسمية مع الأنواع الرسمية الأخرى المقيدة لتمديد "سلسلة"

مثال:


declare function mapValues<p extends string, T1[p], T2[p]>(
     obj:{...p: T1[p]}, fn:(arg: T1[p]) => T2[p]
): {...p: T2[p]};
3. السماح بإتلاف الأنواع الرسمية

مثال:

class C<Array<T>> {
     x: T;
}   

var v1: C<string[]>;   // v1.x: string

مع كل الخطوات الثلاث معًا يمكننا الكتابة

declare module Backbone {

  class Model<{...p: T[p]}> {
    get(prop: p): T[p];
    set(prop: p, value: T[p]): void;
  }
}

declare module ImmutableJS {
  class Map<{...p: T[p]}> {
    get(prop: p): T[p];
    set(prop: p, value: T[p]): this;
  }
}

declare function pluck<p extends string, T[p]>(
    arr: Array<{...p:T[p]}>, prop: p
): Array<T[p]> 

أعلم أن بناء الجملة مطول أكثر من الصيغة التي اقترحهاfdecampredon
لكن لا يمكنني تخيل كيفية التعبير عن نوع mapValues أو combineReducers مع الاقتراح الأصلي.

function combineReducers(reducers) {
  return (state, action) => mapValues(reducers, (reducer, key) => reducer(state[key], action))
}

مع اقتراحي سيبدو إعلانه كما يلي:

declare function combineReducers<p extends string, S[p]>(
    reducers: { ...p: (state: S[p], action: Action) => S[p] }
): (state: { ...p: S[p] }, action: Action) => { ...p: S[p] };

هل يمكن التعبير عنه بالاقتراح الأصلي؟

spion ، fdecampredon ؟

Artazor بالتأكيد تحسن. يبدو الآن أنه جيد بما يكفي للتعبير عن حالة استخدام معياري لهذه الميزة:

بافتراض أن user.id و user.name من أنواع أعمدة قاعدة البيانات التي تحتوي على number و string وفقًا لذلك ، على سبيل المثال Column<number> و Column<string>

اكتب نوع الوظيفة select التي تستغرق:

select({id: user.id, name: user.name})

وإرجاع Query<{id: number; name: string}>

select<T>({...p: Column<T[p]>}):Query<T>

لست متأكدًا من كيفية التحقق من ذلك واستنتاجه. يبدو أنه قد تكون هناك مشكلة ، لأن النوع T غير موجود. هل يجب التعبير عن نوع الإرجاع في شكل محطم؟ بمعنى آخر

select<T>({...p: Column<T[p]>}):Query<{...p:T[p]}>

نسيت Artazor أن تسأل ، هل المعلمة p extends string ضرورية حقًا؟

ألا يكفي شيء مثل هذا؟

select<T[p]>({...p: Column<T[p]>}):Query<{...p:T[p]}>

أي لجميع متغيرات النوع T [p] في {...p:Column<T[p]>}

تحرير: سؤال آخر ، كيف سيتم التحقق من صحة هذا؟

spion لقد حصلت على وجهة نظرك - لقد أساءت فهمي (ولكن هذا خطأي) ، من خلال T[p] كنت أعني شيئًا مختلفًا تمامًا ، وسترى أن p extends string هو شيء حاسم هنا. اسمحوا لي أن أشرح أفكاري بعمق.

لنفكر في أربع حالات استخدام لهياكل البيانات: List ، Tuple ، Dictionary و Record . سنشير إليها على أنها هياكل بيانات مفاهيمية وسنصفها بالخصائص التالية:

هياكل البيانات المفاهيميةالوصول عن طريق
أ. موضع
(رقم)
ب. مفتاح
(خيط)
عد من
العناصر
1. متغير
(نفس الدور لكل عنصر)
قائمةقاموس
2. ثابت
(كل عنصر يلعب دوره الفريد)
توبليسجل

في JavaScript ، يتم دعم هذه الحالات الأربع بنموذجين للتخزين: Array - لـ a1 و a2 ، و Object - لـ b1 و b2 .

_ملاحظة 1 _. لكي نكون صادقين ، ليس من الصحيح أن نقول أن Tuple مدعوم بـ Array ، لأن نوع المجموعة "true" الوحيد هو نوع العنصر arguments الذي ليس Array . ومع ذلك ، فهي قابلة للتبادل إلى حد ما ، خاصة في سياق التدمير و Function.prototype.apply الذي يفتح الباب أمام البرمجة الوصفية. يستفيد TypeScript أيضًا من المصفوفات لنمذجة Tuples.

_ملاحظة 2 _. بينما تم تقديم المفهوم الكامل للقاموس مع المفاتيح التعسفية بعد عقود في شكل ES6 Map ، قرار Brendan Eich الأولي بدمج Record و Dictionary (مع مفاتيح السلسلة فقط) تحت غطاء محرك السيارة نفسه Object (حيث obj.prop يعادل obj["prop"] ) أصبح العنصر الأكثر إثارة للجدل في اللغة بأكملها (في رأيي).
إنها لعنة ونعمة من دلالات JavaScript. إنه يجعل الانعكاس تافهًا ويشجع المبرمجين على التبديل بحرية بين مستويات _البرمجة_ و_البرمجة_الميتة_ بتكلفة ذهنية صفر تقريبًا (حتى بدون ملاحظة!). أعتقد أنه جزء أساسي من نجاح JavaScript كلغة برمجة نصية.

لقد حان الوقت الآن لأن توفر TypeScript طريقة للتعبير عن أنواع لهذه الدلالات الغريبة. عندما نفكر على مستوى _programming_ كل شيء على ما يرام:

أنواع على مستوى البرمجةالوصول عن طريق
أ. موضع
(رقم)
ب. مفتاح
(خيط)
عد من
العناصر
1. متغير
(نفس الدور لكل عنصر)
تي []{[مفتاح: سلسلة]: T}
2. ثابت
(كل عنصر يلعب دوره الفريد)
[T1، T2، ...]{key1: T1، key2: T2، ...}

ومع ذلك ، عندما ننتقل إلى مستوى البرمجة meta ، فإن الأشياء التي تم إصلاحها على مستوى _programming_ تصبح فجأة متغيرة! وهنا نتعرف فجأة على العلاقات بين المجموعات والتوقيعات الوظيفية ونقترح أشياء مثل # 5453 (الأنواع المتغيرة) التي تغطي في الواقع جزءًا صغيرًا فقط (ولكن مهم جدًا) من احتياجات البرمجة الوصفية - تسلسل التوقيعات من خلال إعطاء القدرة على إتلاف شكل رسمي المعلمة إلى أنواع بقية أرجس:

     function f<T>(a: number, ...args:T) { ... }

    f<[string,boolean]>(1, "A", true);

في حالة تنفيذ # 6018 أيضًا ، يمكن أن تبدو فئة الوظيفة مثل

declare class Function<This, TArgs, TRes> {
        This::(...args: TArgs): TRes;
        call(self: This, ...args: TArgs): TRes;
        apply(self: This, args: TArgs): TRes;
        // bind needs also formal pattern matching:
        bind<[...TPartial, ...TCurried] = TArgs>(
           self: This, ...args: TPartial): Function<{}, TCurried, TRes> 
}

إنه شيء رائع ولكن على أي حال غير مكتمل.

على سبيل المثال ، تخيل أننا نريد تعيين نوع صحيح للوظيفة التالية:

function extractAndWrapAll(...args) {
     return args.map(x => [x]);
}

// wrapAll(1,"A",true) === [[1],["A"],[true]]

مع الأنواع المتغيرة المقترحة لا يمكننا تحويل أنواع التوقيع. نحن هنا بحاجة إلى شيء أقوى. في الواقع ، من المستحسن أن يكون لديك وظائف وقت الترجمة التي يمكن أن تعمل على الأنواع (كما في Facebook's Flow). على الرغم من ذلك ، أنا متأكد من أن هذا سيكون ممكنًا فقط عندما (وإذا) سيكون نظام الكتابة من TypeScript مستقرًا بدرجة كافية لتعريضه للمبرمجين النهائيين دون مخاطرة كبيرة لكسر أرض المستخدم في التحديث الثانوي التالي. وبالتالي ، فنحن بحاجة إلى شيء أقل جذرية من دعم البرمجة الوصفية الكامل ولكننا لا نزال قادرين على التعامل مع المشكلات الموضحة.

لمعالجة هذه المشكلة ، أريد أن أقدم فكرة _object signature_. تقريبا هو إما

  1. مجموعة أسماء خصائص الكائن ، أو
  2. مجموعة مفاتيح القاموس ، أو
  3. مجموعة الفهارس الرقمية لمصفوفة
  4. مجموعة المواضع الرقمية لمجموعة.

من الناحية المثالية ، سيكون من الجيد أن يكون لديك عدد صحيح من أنواع حرفية بالإضافة إلى أنواع حرفية سلسلة لتمثيل تواقيع المصفوفات أو مجموعات مثل

type ZeroToFive = 0 | 1 | 2 | 3 | 4 | 5;
// or
type ZeroToFive = 0 .. 5;   // ZeroToFive extends number (!)

تتطابق قواعد الأعداد الصحيحة مع السلسلة الحرفية ؛

ثم يمكننا تقديم صيغة تجريد التوقيع ، والتي تتطابق تمامًا مع بناء جملة الكائن / انتشار:

{...Props: T }

مع استثناء واحد مهم: هنا Props هو المعرف المرتبط تحت المُحدد الكمي العالمي:

<Props extends string> {...Props: T }  // every property has type T
<Index extends number> {...Index: T }  // every item has type T  
// the same as T[]

وبالتالي ، يتم تقديمه في نوع النطاق المعجمي ويمكن استخدامه في أي مكان في هذا النطاق كاسم للنوع. ومع ذلك ، فإن استخدامه مزدوج: عند استخدامه بدلاً من اسم خاصية الراحة / الانتشار ، فإنه يمثل التجريد فوق توقيع الكائن عندما يتم استخدامه كنوع مستقل ، فإنه يمثل نوعًا فرعيًا من السلسلة (أو الرقم على التوالي).

declare class Object {
      static keys<p extends string, q extends p>(object{...q: {}}): p[];
}

وهنا الجزء الأكثر تعقيدًا: _أنواع المعتمدين الرئيسية_

نقدم نوعًا خاصًا: T for Prop (أود استخدام بناء الجملة هذا بدلاً من T [Prop] الذي أربكك) حيث Prop هو اسم متغير النوع الذي يحمل توقيع الكائن. على سبيل المثال ، يقدم <Prop extends string, T for Prop> نوعين رسميين في النطاق المعجمي من النوع ، Prop و T حيث من المعروف أنه لكل قيمة معينة p من Prop سيكون هناك نوع خاص به T .

نحن لا نقول أن في مكان ما كائن له خصائص Props وأنواعها T ! نحن نقدم فقط تبعية وظيفية بين نوعين. يرتبط النوع T بأعضاء من النوع Props ، وهذا كل شيء!

يمنحنا إمكانية كتابة أشياء مثل

function unwrap<P extends string, T for P>(obj:{...P: Maybe<T>}): Maybe<{...P: T}> {
  ...
}

unwrap({a:{value:1}, b:{value:"A"}, c:{value: true}}) === { a: 1, b: "A", c: true }
// here actual parameters will be inferred as 
unwrap<"a"|"b"|"c", {a: number, b: string, c: boolean}>

ومع ذلك ، لا يتم التعامل مع المعلمة الثانية ككائن بل كخريطة مجردة لمعرفات الأنواع. في هذا المنظور ، يمكن استخدام T for P لتمثيل تسلسلات مجردة لأنواع tuples عندما يكون P هو نوع فرعي من الأرقام ( JsonFreeman ؟)

عندما يتم استخدام T مكان ما داخل {...P: .... T .... } فإنه يمثل نوعًا واحدًا محددًا من تلك الخريطة.

إنها فكرتي الرئيسية.
تنتظر بفارغ الصبر أي أسئلة أو أفكار أو نقد -)

حسنًا ، لذلك فإن extends string هو أخذ المصفوفات (والمتغيرات المتغيرة) في الاعتبار في حالة واحدة ، وثوابت السلسلة (كأنواع) في الحالة الأخرى. حلو!

لا نقول أن شيئًا ما له خصائص الدعائم وأنواعها هي T! نحن نقدم فقط تبعية وظيفية بين نوعين. يرتبط النوع T بأعضاء من النوع Props ، وهذا كل شيء!

لم أقصد ذلك ، كنت أعني ذلك أكثر لأن أنواعها هي T [p] ، وهو قاموس للأنواع المفهرسة بواسطة p. إذا كان هذا حدسًا جيدًا ، فسأحتفظ بذلك.

بشكل عام ، قد يحتاج بناء الجملة إلى مزيد من العمل ، لكن الفكرة العامة تبدو رائعة.

هل من الممكن كتابة unwrap للوسائط المتغيرة؟

تحرير: فما باللك ، لقد أدركت للتو أن الامتداد المقترح للأنواع المختلفة يعالج هذا الأمر.

مرحبا جميعا،
أنا محير في كيفية حل إحدى مشكلتي ووجدت هذه المناقشة.
المشكله هي:
لديّ طريقة RpcManager.call(command:Command):Promise<T> ، وسيكون الاستخدام كذلك:

RpcManager.call(new GetBalance(123)).then((result) => {
 // here I want that result would have a type.
});

أعتقد أن الحل يمكن أن يكون مثل:

interface Command<T> {
    responseType:T;
}

class GetBalance implements Command<number> {
    responseType: number; // somehow this should be avoided. maybe Command should be abstract class.
    constructor(userId:number) {}
}

class RpcManager {
    static call(command:Command):Promise<typeof command.responseType> {
    }
}

or:

class RpcManager {
    static call<T>(command:Command<T>):Promise<T> {
    }
}

اي افكار في هذا؟

@ antanas-arvasevicius يجب أن تفعل كتلة الكود الأخيرة في هذا المثال ما تريد.

يبدو أن لديك سؤالاً أكثر عن كيفية إنجاز مهمة معينة ؛ الرجاء استخدام Stack Overflow أو تقديم مشكلة إذا كنت تعتقد أنك عثرت على خطأ في المترجم.

مرحبًا رايان ، شكرًا لك على ردك.
لقد جربت الجزء الأخير من التعليمات البرمجية ولكنه لا يعمل.

عرض سريع:

interface Command<T> { }
class MyCommand implements Command<{status:string}> { }
class RPC { static call<T>(command:Command<T>):T { return; } }

let response = RPC.call(new MyCommand());
console.log(response.status);

//output: error TS2339: Property 'status' does not exist on type '{}'.
//tested with: Version 1.9.0-dev.20160222

آسف لأنني لم أستخدم Stack Overflow ، لكنني اعتقدت أنه مرتبط بهذه المشكلة :)
هل يجب أن أفتح عددًا جديدًا حول هذا الموضوع؟

تمنع معلمة النوع العام غير المستهلكة الاستدلال من العمل ؛ بشكل عام ، يجب ألا يكون لديك أبدًا معلمات نوع غير مستخدمة في تعريفات النوع لأنها بلا معنى. إذا كنت تستهلك T ، فكل شيء يعمل فقط:

interface Command<T> { foo: T }
class MyCommand implements Command<{status:string}> { foo: { status: string; } }
class RPC { static call<T>(command:Command<T>):T { return; } }

let response = RPC.call(new MyCommand());
console.log(response.status);

هذا مذهل! شكرا لك!
لم أعتقد أن معلمة النوع يمكن وضعها داخل النوع العام و
سوف يستخرجه TS.
في 22 فبراير 2016 ، 11:56 مساءً ، كتب "Ryan Cavanaugh" [email protected] :

تمنع معلمة النوع العام غير المستهلكة الاستدلال من العمل ؛ في
عام يجب ألا يكون لديك _ أبدًا_ معلمات نوع غير مستخدمة في النوع
التصريحات لأنها لا معنى لها. إذا كنت تستهلك تي ، كل شيء فقط
يعمل:

أمر الواجهة{foo: T} class MyCommand تنفذ الأمر <{status: string}> {foo: {status: string؛ }} class RPC {static call(الأمر: الأمر): T {return؛ }}
دع الاستجابة = RPC.call (new MyCommand ()) ؛
console.log (response.status) ؛

-
قم بالرد على هذا البريد الإلكتروني مباشرةً أو قم بعرضه على GitHub
https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -187404245
.

@ antanas-arvasevicius إذا كنت تقوم بإنشاء واجهات برمجة تطبيقات على غرار RPC ، فلدي بعض المستندات التي قد تجدها مفيدة: https://github.com/alm-tools/alm/blob/master/docs/contributing/ASYNC.md : rose:

تبدو المناهج أعلاه:

  • معقد إلى حد ما؛
  • لا تعالج استخدام السلاسل في الكود. string ليست قابلة "للعثور على كل المراجع" ، ولا يمكن إعادة تصنيعها (مثل إعادة التسمية).
  • البعض يدعم فقط خصائص "المستوى الأول" وليس التعبيرات الأكثر تعقيدًا (التي تدعمها بعض الأطر).

إليكم فكرة أخرى مستوحاة من أشجار C # Expression. هذه مجرد فكرة تقريبية ، لا شيء مدروس بالكامل! بناء الجملة فظيع. أريد فقط أن أرى ما إذا كان هذا يلهم شخصًا ما.

افترض أن لدينا نوعًا خاصًا من السلاسل للدلالة على التعبيرات.
لنسميها type Expr<T, U> = string .
حيث T هو نوع كائن البداية و U هو نوع النتيجة.

افترض أنه يمكننا إنشاء مثيل لـ Expr<T,U> باستخدام lambda يأخذ معلمة واحدة من النوع T ويؤدي وصول العضو إليها.
على سبيل المثال: person => person.address.city .
عندما يحدث هذا ، يتم تجميع lambda بالكامل إلى سلسلة تحتوي على أي وصول إلى المعلمة ، في هذه الحالة: "address.city" .

يمكنك استخدام سلسلة عادية بدلاً من ذلك ، والتي يمكن رؤيتها على أنها Expr<any, any> .

يتيح الحصول على هذا النوع الخاص Expr في اللغة أشياء مثل:

function pluck<T, U>(array: T[], prop: Expr<T, U>): U[];

let numbers = pluck([{x: 1}, {x: 2}], p => p.x);  // number[]
// compiles to:
// let numbers = pluck([..], "x");

هذا في الأساس شكل محدود لما تُستخدم التعبيرات في C # من أجله.
هل تعتقد أن هذا يمكن تحسينه ويؤدي إلى مكان مثير للاهتمام؟

تضمين التغريدة

_ ( @ jods4 - أنا آسف لأنني لم أرد على اقتراحك هنا. آمل ألا يتم "دفنه" من خلال التعليقات) _

أعتقد أن تسمية هذه الميزة ("نوع خاصية النوع") محيرة للغاية ويصعب فهمها. كان من الصعب جدًا حتى معرفة ما هو المفهوم الموصوف هنا وماذا يعني في المقام الأول!

أولاً ، ليست كل الأنواع لها خصائص! undefined و null (على الرغم من أن هذه ليست سوى إضافات حديثة إلى نظام النوع). نادراً ما تتم فهرسة العناصر الأولية مثل number ، string ، boolean بواسطة خاصية (على سبيل المثال 2 ["prop"]؟ على الرغم من أن هذا يبدو ناجحًا ، إلا أنه غالبًا ما يكون خطأ)

أقترح تسمية مرجع نوع خاصية الواجهة لهذه المشكلة من للإشارة إلى نوع موجود باستخدام متغير سلسلة أو معلمة دالة والتي يجب أن تكون قيمتها معروفة في وقت الترجمة.

كان من الممكن أن يكون مفيدًا للغاية إذا تم وصف ذلك وتمثيله بأكبر قدر ممكن من البساطة ، خارج سياق حالة استخدام معينة:

interface MyInterface {
  prop1: number;
  prop2: string;
}

let prop1Name = "prop1";
type Prop1Type = MyInterface[prop1Name]; // Prop1Type is now 'number'

let prop2Name = "prop2";
type Prop2Type = MyInterface[prop2Name]; // Prop2Type is now 'string'

let prop3Name = "prop3";
type NonExistingPropType = MyInterface[prop3Name]; // Compilation error: property 'prop3' does not exist on 'MyInterface'.

let randomString = createRandomString();
type NotAvailablePropType = MyInterface[randomString]; // Compilation error: value of 'randomString' is not known at compile time.

_Edit: يبدو أنه من أجل تنفيذ هذا بشكل صحيح ، يجب أن يعرف المحول البرمجي على وجه اليقين أن السلسلة المخصصة للمتغير لم تتغير بين النقطة التي تمت تهيئتها والنقطة التي تمت الإشارة إليها في تعبير النوع. لا أعتقد أن هذا سهل للغاية؟ هل سيبقى هذا الافتراض حول سلوك وقت التشغيل دائمًا؟ _

_Edit 2: ربما يعمل هذا فقط مع const عند استخدامه مع متغير؟ _

لست متأكدًا مما إذا كان القصد الأصلي هو السماح فقط بتمرير مرجع الخاصية في الحالة الخاصة جدًا لسلسلة حرفية إلى دالة أو معلمة طريقة؟ على سبيل المثال

function func(someString: string): MyInterface[someString] {
  ..
}

let x = func("prop"); // x gets the type of MyInterface.prop

هل قمت بتعميم هذا بما يتجاوز ما قصدته أصلاً؟

كيف سيتعامل هذا مع الحالة التي تكون فيها وسيطة الوظيفة ليست سلسلة حرفية ، أي غير معروفة في وقت الترجمة؟ على سبيل المثال

let x = func(getRandomString()); // What type if inferred for 'x' here?

هل سيكون خطأ ، أو الافتراضي إلى any ربما؟

(ملاحظة: إذا كانت هذه هي النية حقًا ، فسأقترح إعادة تسمية هذه المشكلة إلى مرجع نوع خاصية الواجهة من خلال وظيفة سلسلة حرفية أو وسيطات طريقة - مع مرور الوقت ، هذا أكثر دقة وتوضيحًا من العنوان الحالي.)

فيما يلي مثال معياري بسيط (يكفي للتوضيح) يوضح ما تحتاجه هذه الميزة لتمكين:

اكتب نوع هذه الوظيفة ، والتي:

  • يأخذ كائنًا يحتوي على حقول وعد ، و
  • إرجاع نفس الكائن ولكن مع حل جميع الحقول ، كل حقل له النوع المناسب.
function awaitObject(obj) {
  var result = {};
  var wait = Object.keys(obj)
    .map(key => obj[key].then(val => result[key] = val));
  return Promise.all(wait).then(_ => result)
}

عند استدعاء الكائن التالي:

var res = awaitObject({a: Promise.resolve(5), b: Promise.resolve("5")})

يجب أن تكون النتيجة res من النوع {a: number; b: string}


باستخدام هذه الميزة (كما تم تجسيدها بالكامل بواسطةArtazor) سيكون التوقيع

awaitObject<p, T[p]>(obj: {...p: Promise<T[p]>}):Promise<{...p: T[p]}>

تحرير: تم إصلاح نوع الإرجاع أعلاه ، فقد Promise<...> ^^

spion

شكرًا لمحاولتك تقديم مثال ، ولكن لم أجد هنا في أي مكان هنا مواصفات معقولة وموجزة ومجموعة من الأمثلة المختصرة الواضحة والمنفصلة عن التطبيقات العملية ومحاولة تسليط الضوء على الدلالات والحالات المختلفة لكيفية عمل ذلك ، حتى شيء أساسي مثل:

function func<T extends object>(name: string): T[name] {
 ...
}
  1. لم يكن هناك سوى القليل من الجهد لتوفير عنوان فعال وتوضيحي (كما ذكرت أن "نوع خاصية النوع" أمر محير وليس اسمًا توضيحيًا لهذه الميزة ، والتي لا تتعلق حقًا بأي نوع معين بل تتعلق بمرجع النوع). كان أفضل عنوان أقترحه هو مرجع نوع خاصية الواجهة بواسطة دالة سلسلة حرفية أو وسيطات طريقة (بافتراض أنني لم أسيء فهم نطاق هذا).
  2. لم يكن هناك ذكر واضح لما سيحدث إذا كان name غير معروف في وقت الترجمة ، أو فارغ ، أو غير محدد ، على سبيل المثال func<MyType>(getRandomString()) ، func<MyType>(undefined) .
  3. لم تكن هناك إشارة واضحة لما سيحدث إذا كان T نوعًا بدائيًا مثل number أو null .
  4. لم يتم تقديم تفاصيل واضحة حول ما إذا كان هذا ينطبق فقط على الوظائف وما إذا كان يمكن استخدام T[name] في جسم الوظيفة. وفي هذه الحالة ، ماذا يحدث إذا أعيد تعيين name في جسم الوظيفة وبالتالي قد لا تكون قيمته معروفة في وقت الترجمة؟
  5. لا توجد مواصفات حقيقية للبنية: على سبيل المثال ، هل يعمل T["propName"] أو T[propName] بنفسه؟ بدون إشارة إلى معلمة أو متغير ، أعني - قد يكون مفيدًا!
  6. لا يوجد ذكر أو مناقشة إذا كان يمكن استخدام T[name] في معلمات أخرى أو حتى خارج نطاقات الوظائف ، على سبيل المثال
function func<T extends object>(name: string, val: T[name]) {
 ...
}
type A = { abcd: number };
const name = "abcd";
let x: A[name]; // Type of 'x' resolves to 'number'

_7. لا توجد مناقشة حقيقية للحل البديل البسيط نسبيًا باستخدام معلمة عامة إضافية و typeof (على الرغم من ذكر ذلك ، ولكن متأخرًا نسبيًا بعد قبول الميزة بالفعل):

function get<T, V>(obj: T, propName: string): V {
    return obj[propName];
}

type MyType = { abcd: number };
let x: MyType  = { abcd: 12 };

let result = get<MyType, typeof x.abcd>(x, "abcd"); // Type of 'result' is 'number'

في الختام: لا يوجد اقتراح حقيقي هنا ، فقط مجموعة من عروض حالة الاستخدام. أنا مندهش من أن هذا قد تم قبوله أو حتى تلقى اهتمامًا إيجابيًا من قبل فريق TypeScript على الإطلاق لأنني لم أكن لأعتقد أن هذا سيكون وفقًا لمعاييرهم. أنا آسف إذا كنت قد أبدو قاسيًا بعض الشيء هنا (ليس نموذجيًا بالنسبة لي) ولكن لا يوجد نقد شخصي أو موجه إلى أي فرد معين.

هل نريد حقًا أن نذهب بعيدًا إلى هذا الحد فيما يتعلق بالبرمجة الوصفية؟

JS هي لغة ديناميكية ، يمكن للكود معالجة الكائنات بطرق عشوائية.
هناك حدود لما يمكننا تصميمه في نظام الكتابة ومن السهل التوصل إلى وظائف لا يمكنك تصميمها في TS.
بل السؤال هو: إلى أي مدى من المعقول والمفيد الذهاب؟

المثال الأخير ( awaitObject ) من spion في نفس الوقت:

  • _معقد جدًا_ من وجهة نظر اللغة (أتفق مع malibuzios هنا).
  • _ضيق جدًا_ من حيث التطبيق. هذا المثال يختار قيودًا محددة ولا يُعمم جيدًا.

راجع للشغل spion ، لم تحصل على نوع الإرجاع الصحيح للمثال الخاص بك ... إنه يُرجع Promise .

نحن بعيدون عن المشكلة الأصلية ، والتي كانت تتعلق بكتابة واجهة برمجة التطبيقات التي تأخذ سلسلة تمثل الحقول ، مثل _.pluck
يجب دعم واجهات برمجة التطبيقات (API) هذه لأنها شائعة وليست معقدة. لا نحتاج إلى نماذج وصفية مثل { ...p: T[p] } لذلك.
هناك العديد من الأمثلة في OP ، وبعضها أكثر من Aurelia في تعليقي باسم المشكلة .
يمكن أن يغطي نهج مختلف حالات الاستخدام هذه ، انظر تعليقي أعلاه للحصول على فكرة محتملة.

@ jods4 ليس ضيقا على الإطلاق. يمكن استخدامه لعدد من الأشياء التي يستحيل نمذجة في نظام الكتابة في الوقت الحالي. أود أن أقول إنها واحدة من آخر الأجزاء المفقودة الكبيرة في نظام نوع TS. هناك الكثير من أمثلة العالم الحقيقي أعلاه من خلال Artazor https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -177287714 الأشياء الأبسط المذكورة أعلاه ، مثال "تحديد" منشئ استعلام SQL وما إلى ذلك.

هذا awaitObject في بلوبيرد. إنها طريقة حقيقية. النوع حاليا عديم الفائدة.

سيكون الحل الأكثر عمومية هو الأفضل. ستعمل مع كل من الحالات البسيطة والمعقدة. سيتم العثور على حل ضعيف القوة ناقص لنصف الحالات وسوف يسبب مشاكل التوافق بسهولة إذا كان من الضروري اعتماد حل أكثر عمومية في المستقبل.

نعم ، إنها تحتاج إلى الكثير من العمل ، لكنني أعتقد أيضًا أن Artazor قام بعمل ممتاز في تحليل واستكشاف جميع الجوانب التي لم نفكر فيها من قبل. لن أبقى فقد ابتعدنا عن المشكلة الأصلية ، لدينا فقط فهم أفضل لها.

قد نحتاج إلى اسم أفضل لها ، سأحاول ولكني عادة لا أفهم هذه الأشياء بشكل صحيح. ماذا عن "الكائنات العامة"؟

spion ، فقط من أجل التصحيح:

awaitObject<p,T[p]>(obj: {...p:Promise<T[p]>}): Promise<{...p:T[p]}>

(لقد فاتك وعد في توقيع الإخراج)
-)

@ jods4 ، أتفق مع spion
هناك الكثير من مشاكل العالم الحقيقي حيث { ...p: T[p] } هو الحل. لتسمية واحدة: رد فعل + تدفق / إعادة

تضمين التغريدة
أنا قلق فقط من أن يصبح تحديد هذا الأمر معقدًا للغاية.

لقد انحرف الدافع وراء هذه القضية. في الأصل كان الأمر يتعلق بدعم واجهات برمجة التطبيقات التي تقبل سلسلة للإشارة إلى حقل كائن. يتم الآن مناقشة واجهات برمجة التطبيقات التي تستخدم الكائنات كخرائط أو سجلات بطريقة ديناميكية للغاية. لاحظ أنه إذا تمكنا من قتل عصفورين بحجر واحد ، فأنا جميعًا مع ذلك.

إذا عدنا إلى واجهات برمجة التطبيقات التي تأخذ مشكلة السلاسل ، فإن T[p] ليس حلاً كاملاً في رأيي (لقد قلت السبب من قبل).

_ فقط من أجل الصواب_ يجب أن يقبل أيضًا awaitObject الخصائص غير التابعة للوعد ، على الأقل يقبل Bluebird props . إذن لدينا الآن:

awaitObject<p,T[p]>(obj: { ...p: T[p] | Promise<T[p]> }): Promise<T>

لقد غيرت نوع الإرجاع إلى Promise<T> لأنني أتوقع أن يعمل هذا الترميز.
هناك أحمال زائدة أخرى ، على سبيل المثال ، شخص يأخذ وعدًا لمثل هذا الكائن (توقيعه أكثر متعة). هذا يعني أن الترميز { ...p } يجب اعتباره تطابقًا أسوأ من أي نوع آخر.

تحديد كل هذا سيكون عملاً شاقاً. أود أن أقول إنها الخطوة التالية إذا كنت تريد دفع هذا إلى الأمام.

تضمين التغريدة

أردت أن أذكر أن هذه الميزة لا تتعلق بالأدوية ولا تتعلق بالنوع العالي من الأنواع متعددة الأشكال. هذا مجرد بناء جملة للإشارة إلى نوع الخاصية من خلال نوع يحتوي (مع بعض الامتدادات "المتقدمة" الموضحة أدناه) ، وهو ليس بعيدًا عن مفهوم typeof . مثال:

type MyType = { abcd: number };
let y: MyType["abcd"]; // Technically this could also be written as MyType.abcd

قارن الآن بـ:

type MyType = { abcd: number };
let x: MyType;

let y: typeof x.abcd;

هناك نوعان من الاختلافات الرئيسية مع typeof . بخلاف typeof ، ينطبق هذا على الأنواع (على الأقل الأنواع غير البدائية ، وهي حقيقة غالبًا ما يتم حذفها هنا) بدلاً من الحالات. بالإضافة إلى ذلك (وهذا شيء رئيسي) تم تمديده لدعم استخدام ثوابت السلسلة الحرفية (والتي يجب أن تكون معروفة في وقت الترجمة) كواصفات مسار الخاصية ، والأدوية:

const propName = "abcd";
let y: MyType[propName];
// Or with a generic parameter:
let y: T[propName];

ومع ذلك ، من الناحية الفنية ، كان من الممكن تمديد typeof لدعم القيم الحرفية للسلسلة أيضًا (وهذا يشمل الحالة التي يكون فيها x نوعًا عامًا):

let x: MyType;
const propName = "abcd";
let y: typeof x[propName];

وبهذا الامتداد يمكن استخدامه أيضًا لحل بعض الحالات المستهدفة هنا:

function get<T>(propName: string, obj: T): typeof obj[propName]

ومع ذلك ، فإن typeof أقوى لأنه يدعم كمية غير محددة من مستويات التداخل:

let y: typeof x.prop.childProp.deeperChildProp

وهذا المستوى واحد فقط. أي ليس مخططًا (على حد علمي) لدعم:

let y: MyType["prop"]["childProp"]["deeperChildProp"];
// Or alternatively
let y: MyType["prop.childProp.deeperChildProp"];

أعتقد أن نطاق هذا الاقتراح (إذا كان من الممكن حتى تسمية مقترح بمستوى غموضه) ضيق للغاية. قد يساعد في حل مشكلة معينة (على الرغم من أنه قد تكون هناك حلول بديلة) ، والذي يبدو أنه يجعل الكثير من الناس حريصين جدًا على الترويج لها. ومع ذلك ، فهي تستهلك أيضًا بناء جملة قيمًا من اللغة. بدون خطة أوسع ونهج موجه نحو التصميم ، يبدو من غير الحكمة تقديم شيء على عجل لا يحتوي حتى على مواصفات واضحة.

_Edits: تصحيح بعض الأخطاء الواضحة في أمثلة التعليمات البرمجية_

لقد بحثت قليلاً عن البديل typeof :

دعم اللغة المستقبل ل typeof x["abcd"] و typeof x[42] تمت الموافقة ، ويقع الآن تحت # 6606، والذي هو حاليا قيد التطوير (هناك تنفيذ العمل).

هذا يذهب في منتصف الطريق. مع وجود هذه الأشياء في مكانها الصحيح ، يمكن القيام بالباقي على عدة مراحل:

(1) إضافة دعم للثوابت الحرفية للسلسلة (أو حتى الثوابت الرقمية - قد تكون مفيدة للبطاقات؟) كمحددات خاصية في تعبيرات النوع ، على سبيل المثال:

let x: MyType;
const propName = "abcd";
let y: typeof x[propName];

(2) السماح بتطبيق هذه المحددات على الأنواع العامة

let x: T; // Where T should extend 'object'
const propName = "abcd";
let y: typeof x[propName];

(3) اسمح بتمرير هذه المحددات ، وعمليًا "إنشاء مثيل" للمراجع ، من خلال وسيطات الوظيفة (مخطط typeof list[0] لذلك أعتقد أن هذا يمكن أن يغطي حالات أكثر تعقيدًا مثل pluck :

function get<T extends object>(obj: T, propertyName: string): typeof obj[propertyName];
function pluck<T extends object>(list: Array<T>, propertyName: string): Array<typeof list[0][propertyName]>;

(النوع object هنا هو النوع المقترح في # 1809)

على الرغم من أنه أكثر قوة (على سبيل المثال قد يدعم typeof obj[propName][nestedPropName] ) ، فمن الممكن ألا يغطي هذا النهج البديل جميع الحالات الموضحة هنا. يرجى إعلامي إذا كانت هناك أمثلة لا يبدو أنها يمكن التعامل معها من خلال هذا (أحد السيناريوهات التي تتبادر إلى الذهن هو عندما لا يتم تمرير مثيل الكائن على الإطلاق ، ومع ذلك ، يصعب علي في هذه المرحلة تخيل حالة ستكون هناك حاجة ، على الرغم من أنني أعتقد أن هذا ممكن).

_تعديلات: تصحيح بعض الأخطاء في الكود_

تضمين التغريدة
بعض الأفكار:

  • يعمل هذا على إصلاح تعريف API ولكنه لا يساعد المتصل. ما زلنا بحاجة إلى نوع من nameof أو Expression في موقع الاتصال.
  • إنه يعمل مع تعريفات .d.ts ، لكنه عمومًا لن يكون قابلاً للاستدلال عليه بشكل ثابت أو حتى يمكن التحقق منه في وظائف / تطبيقات TS.

كلما فكرت في الأمر ، كلما بدا الأمر أكثر من Expression<T, U> كحل لهذا النوع من واجهات برمجة التطبيقات. يعمل على إصلاح مشاكل موقع الاتصال ، وكتابة النتيجة ويمكن الاستدلال عليها + التحقق منها في التنفيذ.

@ jods4

لقد ذكرت بطريقة التعليق السابقة أعلاه أنه يمكن تنفيذ pluck باستخدام التعبيرات. على الرغم من أنني اعتدت أن أكون على دراية كبيرة بـ C # ، إلا أنني لم أجربها أبدًا ، لذلك أعترف أنني لا أملك حقًا فهمًا جيدًا لها في هذه المرحلة.

على أي حال ، أردت فقط أن أذكر أن TypeScript يدعم _استدلال وسيطة النوع_ ، لذا بدلاً من استخدام pluck ، يمكن للمرء فقط استخدام map وتحقيق نفس النتيجة ، والتي لن تتطلب حتى تحديد معلمة عامة ( تم استنتاجه) وسيعطي أيضًا فحصًا وإكمالًا كاملاً للنوع:

let x = [{name: "John", age: 34}, {name: "Mary", age: 53}];

let result = x.map(obj => obj.name);
// 'result' is ["John", "Mary"] and its type inferred as 'string[]' 

حيث يتم الإعلان عن map (مبسط للغاية للشرح)

map<U>(mapFunc: (value: T) => U): U[];

يمكن استخدام نمط الاستدلال نفسه هذا "كخدعة" لتمرير أي نتيجة لـ lambda التعسفي (والتي لا تحتاج حتى إلى استدعاء) إلى دالة وتعيين نوع الإرجاع لها:

function thisIsATrick<T, U>(obj: T, onlyPassedToInferTheReturnType: () => U): U {
   return;
}

let x = {name: "John", age: 34};

let result = thisIsATrick(x, () => x.age) // Result inferred as 'number' 

_Edit: قد يبدو من السخف تمريرها في لامدا هنا وليس الشيء نفسه! ولكن في الحالات الأكثر تعقيدًا مثل الكائنات المتداخلة (على سبيل المثال x.prop.Childprop ) قد تكون هناك مراجع غير محددة أو فارغة قد تكون خطأ. وجوده في لامدا ، وهو ما لا يُسمى بالضرورة ، يتجنب ذلك.

أعترف أنني لست على دراية جيدة ببعض حالات الاستخدام التي تمت مناقشتها هنا. أنا شخصياً لم أشعر أبداً بالحاجة إلى استدعاء دالة تأخذ اسم خاصية كسلسلة. عادةً ما يكون تمرير لامدا (حيث يتم الاحتفاظ بالمزايا التي تصفها أيضًا) مع واجهة وسيطة من النوع كافياً لحل العديد من المشكلات الشائعة.

السبب في أنني اقترحت أن النهج البديل typeof كان في الغالب لأن ذلك يبدو أنه يغطي حالات الاستخدام نفسها ويوفر نفس الوظيفة لما يصفه الناس هنا. هل يمكنني استخدامه بنفسي؟ لا أعرف ، لم أشعر أبدًا بالحاجة الحقيقية لذلك (قد يكون الأمر ببساطة أنني لم أستخدم مكتبات خارجية مثل الشرطة السفلية ، فأنا دائمًا ما أقوم بتطوير وظائف المنفعة بنفسي ، حسب الحاجة).

malibuzios صحيح ، pluck سخيف بعض الشيء مع lambdas + map مدمج الآن.

في هذا التعليق ، أعطي 5 واجهات برمجة تطبيقات مختلفة تأخذ جميعها سلاسل - كلها متصلة بمراقبة التغييرات.

@ jods4 وغيرها

أردت فقط أن أذكر أنه عند اكتمال # 6606 ، من خلال تنفيذ المرحلة 1 فقط (السماح باستخدام حرفية السلسلة الثابتة كمحددات خصائص) ، يمكن تحقيق بعض الوظائف المطلوبة هنا ، وإن لم تكن بأناقة كما يمكن (قد تتطلب يتم التصريح عن اسم الخاصية على أنه ثابت ، مع إضافة عبارة إضافية).

function observeProperty<T, U>(obj: T, propName: string ): Subscriber<U> {
    ....
}

let x = { name: "John", age: 42 };

const propName = "age";
observeProperty<typeof x, typeof x[propName]>(x, propName);

ومع ذلك ، قد يكون مقدار الجهد المبذول لتنفيذ ذلك أقل بكثير من تنفيذ المرحلتين 2 و 3 أيضًا (لست متأكدًا ولكن من الممكن أن يكون 1 قد تم تغطيته بالفعل بواسطة # 6606). مع تنفيذ المرحلة 2 ، سيغطي هذا أيضًا الحالة التي يكون فيها x نوعًا عامًا (لكنني لست متأكدًا مما إذا كان هذا مطلوبًا حقًا؟).

تعديل: السبب في أنني استخدمت ثابتًا خارجيًا ولم أكتفي فقط بكتابة اسم الخاصية مرتين لم يكن فقط تقليل الكتابة ، ولكن أيضًا لضمان تطابق الأسماء دائمًا ، على الرغم من أنه لا يزال لا يمكن استخدامه مع أدوات مثل "إعادة التسمية" و "العثور على الكل المراجع "، والتي أجدها عيبًا خطيرًا.

@ jods4 ما زلت أبحث عن حل أفضل لا يستخدم السلاسل. سأحاول التفكير في ما يمكن فعله باستخدام nameof ومتغيراته.

هذه فكرة أخرى.

نظرًا لأن السلاسل الحرفية مدعومة بالفعل كأنواع ، على سبيل المثال ، يمكن للمرء أن يكتب بالفعل:

let x: "HELLO"; 

يمكن للمرء أن يرى سلسلة حرفية تم تمريرها إلى وظيفة كشكل من أشكال التخصص العام

(_Edit: تم تصحيح هذه الأمثلة للتأكد من أن s غير قابل للتغيير في جسم الوظيفة ، hough const غير مدعوم في مواضع المعلمات في هذه المرحلة (لست متأكدًا من readonly على أية حال)._) :

function func(const s: string)

يمكن هنا عرض نوع المعلمة string المرتبط بـ s باعتباره عامًا ضمنيًا (حيث يمكن أن يكون متخصصًا في القيم الحرفية للسلسلة). من أجل الوضوح ، سأكتبها بشكل أكثر وضوحًا (على الرغم من أنني لست متأكدًا مما إذا كانت هناك حاجة فعلاً إلى ذلك):

function func<S extends string>(const s: S)
func("TEST");

يمكن أن يتخصص داخليًا في:

function func(s: "TEST")

والآن يمكن استخدام هذا كطريقة بديلة "لتمرير" اسم الخاصية ، والذي أشعر أنه يجسد الدلالات بشكل أفضل هنا.

function observeProperty<T, S extends string>(obj: T, const propName: S): Subscriber<T[S]>

x = { name: "John",  age: 33};

observeProperty(x, nameof(x.age))

نظرًا لأنه في T[S] ، فإن كلا من T و S معلمات عامة. يبدو من الطبيعي أن يكون هناك نوعان مدمجان في موضع نوع بدلاً من خلط وقت التشغيل وعناصر نطاق النوع (مثل T[someString] ).

_Edits: إعادة كتابة أمثلة للحصول على نوع معلمة سلسلة غير قابلة للتغيير.

نظرًا لأنني أستخدم TS 1.8.7 وليس أحدث إصدار ، لم أكن أدرك ذلك في الإصدارات الأحدث في

const x = "Hello";

تم بالفعل استنتاج نوع x كنوع حرفي "Hello" ، (على سبيل المثال: x: "Hello" ) وهو أمر منطقي للغاية (انظر # 6554).

لذلك ، بطبيعة الحال ، إذا كانت هناك طريقة لتعريف المعلمة على أنها const (أو ربما readonly ستعمل أيضًا؟):

function func<S extends string>(const s: S): S

ثم أعتقد أن هذا يمكن أن يصمد:

let result = func("abcd"); // type of 'result' inferred as the literal type "abcd"

نظرًا لأن بعضًا من هذا جديد نوعًا ما ويعتمد على ميزات اللغة الحديثة ، سأحاول تلخيصه بأكبر قدر ممكن من الوضوح:

(1) عندما يتلقى متغير سلسلة const (وربما readonly أيضًا؟) قيمة معروفة في وقت الترجمة ، يتلقى المتغير تلقائيًا نوعًا حرفيًا له نفس القيمة ( أعتقد أن هذا سلوك حديث لم يحدث في 1.8.x ) ، على سبيل المثال

const x = "ABCD";

يُستنتج أن نوع x هو "ABCD" ، وليس string ! ، على سبيل المثال ، يمكن للمرء أن يقول هذا x: "ABCD" .

(2) إذا تم السماح بمعلمات الدالة readonly المعلمة readonly ذات النوع العام ستتخصص بشكل طبيعي في نوعها لسلسلة حرفية عند تلقيها كوسيطة ، حيث أن المعلمة غير قابلة للتغيير!

function func<S extends string>(readonly str: S);
func("ABCD");

هنا تم حل S إلى النوع "ABCD" ، وليس string !

ومع ذلك ، إذا كانت المعلمة str غير قابلة للتغيير ، فلن يضمن المترجم عدم إعادة تعيينها في نص الوظيفة ، وبالتالي ، سيكون النوع المستنتج هو string .

function func<S extends string>(str: S) {
    str = "DCBA"; // This may happen
}

func("ABCD");

(3) من الممكن الاستفادة من هذا وتعديل الاقتراح الأصلي ليكون محدد الخاصية مرجعًا لنوع ، والذي يجب أن يكون مقيدًا ليكون مشتقًا من string (في بعض الحالات يكون ذلك قد يكون من المناسب حصرها في الأنواع الحرفية المفردة فقط ، على الرغم من عدم وجود طريقة فعلية للقيام بذلك) ، بدلاً من كيان وقت التشغيل:

function get<T extends object, S extends string>(obj: T, readonly propName: S): T[S]

استدعاء هذا لا يتطلب تحديد أي نوع من الوسائط بشكل صريح ، حيث يدعم TypeScript الاستدلال على وسيطة النوع:

let x = { name: "John", age: 42 };

get(x, "age"); // result type is inferred to be 'number'
// or for stronger type safety:
get(x, nameof(x.age)); // result type is inferred to be 'number'

_Edits: تصحيح بعض الأخطاء الإملائية والتعليمية.
_ملاحظة: نسخة معممة وموسعة من هذا النهج المعدل يتم تتبعها الآن في # 7730._

إليك استخدام آخر لنوع خاصية النوع (أو "الأدوية المفهرسة" كما أحب أن أسميها مؤخرًا) ظهرت في مناقشة معRaynos

يمكننا بالفعل كتابة المدقق العام التالي للمصفوفات:

function tArray<T>(f:(t:any) => t is T) {
    return function (a:any): a is Array<T> {
        if (!Array.isArray(a)) return false;
        for (var k = 0; k < a.length; ++k)
            if (!f(a[k])) return false;
        return true;
    }
}

function tNumber(n:any): n is number {
    return typeof n === 'number'
}
var isArrayOfNumber = tArray(tNumber)

function test(x: {}) {
    if (isArrayOfNumber(x)) {
        return x[x.length - 1].toFixed(2); // this type checks
    }
}

بمجرد أن نقوم بفهرسة الأدوية ، سنكون قادرين أيضًا على كتابة مدقق عام للكائنات:

function tObject<p, T[p]>(checker: {...p: (t:any) => t is T[p]}) {
  return function(obj: any): obj is {...p: T[p] } {
    for (var key in checker) if (!checker[key](obj[key])) return false;
    return true;
  }
}

التي ستتيح لك ، جنبًا إلى جنب مع المدققات البدائية للسلسلة ، والرقم ، والمنطقية ، والخالية وغير المحددة ، كتابة أشياء مثل هذه:

var isTodoList = tObject({
  items: tArray(tObject({text: tString, completed: tBoolean})),
  showCompleted: tBoolean
})

والحصول على نتيجة باستخدام مدقق وقت التشغيل الصحيح _ و_ تجميع نوع الوقت حارس ، في نفس الوقت :)

هل قام أي شخص بأي عمل على هذا حتى الآن ، أم أنه على رادار أي شخص؟ سيكون هذا تحسينًا كبيرًا لعدد المكتبات القياسية التي يمكنها استخدام الكتابة ، بما في ذلك أمثال lodash أو ramda والعديد من واجهات قواعد البيانات.

malibuzios أعتقد أنك تقترب من اقتراح Artazor :)

لمعالجة مخاوفك:

function func<S extends string>(readonly str: S): T[str] {
 ...
}

هذا سوف يكون

function func<S extends string, T[S]>(str: S):T[S] { }

بهذه الطريقة ، سيتم قفل الاسم على النوع الأكثر تحديدًا (نوع ثابت سلسلة) عندما يتم استدعاؤه باستخدام:

func("test")

النوع S يصبح "test" (وليس string ). على هذا النحو ، لا يمكن إعادة تعيين قيمة مختلفة لـ str . إذا حاولت ذلك ، على سبيل المثال

str = "other"

يمكن أن ينتج عن المترجم خطأ (ما لم تكن هناك مشكلات تتعلق بسلامة التباين ؛))

بدلاً من مجرد الحصول على نوع خاصية واحدة ، أود الحصول على خيار الحصول على نوع تعسفي من النوع الفائق.

لذا فإن اقتراحي هو إضافة الصيغة التالية: بدلاً من مجرد الحصول على T[prop] ، أود إضافة الصيغة T[...props] . حيث يجب أن يكون props مصفوفة من أعضاء T . والنوع الناتج هو نوع ممتاز من T مع أعضاء من T محدد في props .

أعتقد أنه سيكون مفيدًا للغاية في Sequelize - وهو ORM شائع لـ node.js. لأسباب تتعلق بالأمان ولأسباب تتعلق بالأداء ، من الحكمة الاستعلام عن السمات في الجدول الذي تحتاج إلى استخدامه. هذا يعني غالبًا نوعًا فائقًا.

interface IUser {
    id: string;
    name: string;
    email: string;
    createdAt: string;
    updatedAt: string;
    password: string;
    // ...
}

interface Options<T> {
     attributes: (memberof T)[];
}

interface Model<IInstance> {
     findOne(options: Options<IInstance>): IInstance[...options.attributes];
}

declare namespace DbContext {
   define<T>(): Model<T>;
}

const Users = DbContext.define<IUser>({
   id: { type: DbContext.STRING(50), allowNull: false },
   // ...
});

const user = Users.findOne({
    attributes: ['id', 'email', 'name'],
    where: {
        id: 1,
    }
});

user.id
user.email
user.name

user.password // error
user.createdAt // error
user.updatedAt // error

(في المثال الخاص بي ، يتضمن عامل التشغيل memberof ، وهو ما تتوقعه أن يكون ، وكذلك التعبير options.attributes وهو نفس التعبير typeof options.attributes ، لكنني أعتقد أن عامل التشغيل typeof زائد عن الحاجة في هذه الحالة ، لأنه في مركز يتوقع نوعًا ما.).

إذا لم يصر أحد ، بدأت العمل على هذا.

ما رأيك في نوع الأمان داخل الوظيفة ، أي ضمان إرجاع عبارة الإرجاع شيئًا يمكن تخصيصه لنوع الإرجاع؟

interface A {
     a: string;
}
function f(p: string): A[p] {
    return 'aaa'; // This is string, but can we ensure it is the intended A[p] ?
}

كما يبدو أن الاسم "نوع الخاصية" المستخدم هنا غير صحيح بعض الشيء. إنها تنص نوعًا ما على أن النوع له خصائص ، تمتلكها جميع الأنواع تقريبًا.

ماذا عن "نوع الملكية المشار إليه"؟

ما رأيك في نوع الأمان داخل الوظيفة

العصف الذهني:

let a: A;
function f(p: string): A[p] {
  let x = a[p]; // typeof A[p], only when:
  // 1. p is directly referencing function argument
  // 2. function return type is Property Reference Type

  p = "abc"; // not allowed to assign a new value when p is used on Property Reference Type

  return x; // x is A[p], so okay
}

وعدم السماح بالخيط العادي على خط العودة.

مشكلة malibuzios في فكرتك هي أنها تتطلب تحديد جميع الأدوية الجنسية في حالة عدم إمكانية استنتاجها ، والتي يمكن أن تكون فيروسية / تلوث قاعدة الشفرة.

هل من تعليقات من فريق TS حول https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -239653337؟

RyanCavanaughmhegazy وما إلى ذلك؟

فيما يتعلق بالاسم ، بدأت في استدعاء هذه الميزة (على الأقل النموذج الذي اقترحه Artazor ) "المفهرسة Generics"

يمكن أن يكون الحل من زاوية أخرى لهذه المشكلة. لست متأكدًا مما إذا كان قد تم إحضاره بالفعل ، فهو موضوع طويل. تطوير اقتراح عام لسلسلة ، يمكننا توسيع توقيع الفهرسة. نظرًا لأنه يمكن استخدام القيم الحرفية للسلسلة لنوع المفهرس ، فيمكننا جعلها مكافئة (كما أعلم أنها ليست كذلك في الوقت الحالي):

interface A1 {
    a: number;
    b: boolean;
}
interface A2 {
    [index: "a"]: number;
    [index: "b"]: boolean;
}

لذلك ، يمكننا فقط الكتابة بعد ذلك

declare function pluck<P, T extends { [indexer: P]: R; }, R>(obj: T, p: P): R;

هناك بعض الأشياء التي يجب وضعها في الاعتبار:

  • كيف أشير إلى أن P يمكن أن يكون سلسلة حرفية فقط؟

    • نظرًا لأن السلسلة توسع تقنيًا جميع السلاسل الحرفية ، فلن يكون P extends string أيديوماتيكيًا جدًا

    • تجري مناقشة أخرى حول السماح بقيد P super string (# 7265 ، # 6613 ، https://github.com/Microsoft/TypeScript/issues/6613#issuecomment-175314703)

    • هل نحن حقا بحاجة إلى الاهتمام بهذا؟ يمكننا استخدام كل ما هو مقبول لنوع المفهرس - إذا كان لدى T مفهرس string s أو number s. ثم P يمكن أن يكون string أو number .

  • حاليًا ، إذا مررنا "something" كوسيطة ثانية ، فسيكون من النوع string

    • قد تكون السلسلة الحرفية # 10195 هي الحل

    • أو يمكن أن يستنتج TS أنه يجب استخدام P أكثر تحديدًا إذا كان مقبولًا

  • المفهرسات اختيارية ضمنيًا { [i: string]: number /* | undefined */ }

    • كيف نفرض إذا كنا لا نريد حقًا أن يكون لدينا undefined في المجال؟

  • الاستدلال التلقائي للنوع لجميع العلاقات بين P و T و R هو مفتاح لجعله يعمل

weswighammhegazy، ولقد تم مناقشة هذا مؤخرا. سنخبرك بأي تطورات نواجهها ، وتذكر أن هذا مجرد نموذج أولي للفكرة.

الأفكار الحالية:

  • عامل تشغيل keysof Foo ينتزع اتحاد أسماء الخصائص من Foo كأنواع حرفية للسلسلة.
  • نوع Foo[K] الذي يحدد أنه بالنسبة لنوع ما K يكون هذا النوع عبارة عن أنواع حرفية للسلسلة أو اتحاد أنواع حرفية للسلسلة.

من هذه الكتل الأساسية ، إذا كنت بحاجة إلى استنتاج سلسلة حرفية كنوع مناسب ، يمكنك الكتابة

function foo<T, K extends keysof T>(obj: T, key: K): T[K] {
    // ...
}

هنا ، سيكون K نوعًا فرعيًا من keysof T مما يعني أنه نوع سلسلة حرفية أو اتحاد أنواع حرفية للسلسلة. أي شيء تقوم بتمريره للمعلمة key يجب كتابته حسب السياق من خلال ذلك الحرفي / اتحاد القيم الحرفية ، واستنتاجه كنوع حرفي لسلسلة فردية.

على سبيل المثال

interface HelloWorld { hello: any; world: any; }

function foo<K extends keysof HelloWorld>(key: K): K {
    return key;
}

// 'x' has type '"hello"'
let x = foo("hello");

أكبر مشكلة هي أن keysof يحتاج غالبًا إلى "تأخير" تشغيله. إنه حريص جدًا على كيفية تقييمه لنوع ما ، وهي مشكلة بالنسبة لمعلمات النوع كما في المثال الأول الذي نشرته (على سبيل المثال ، الحالة التي نريد حلها هي في الواقع الجزء الصعب: الابتسامة :).

اتمنى ان يعطيك كل تحديث

DanielRosenwasser شكرا على التحديث. لقد رأيت للتو weswigham قد keysof ، لذلك ربما يكون من الأفضل تسليم هذه المشكلة إليكم يا رفاق.

أنا فقط أتساءل لماذا قررت الخروج من الصيغة الأصلية المقترحة؟

function get(prop: string): T[prop];

وتقديم keysof ؟

T[prop] أقل عمومية ، ويتطلب الكثير من الآلات المتداخلة. أحد الأسئلة المهمة هنا هو كيف يمكنك ربط المحتويات الحرفية لـ prop بأسماء العقارات T . لست متأكدًا تمامًا مما ستفعله. هل يمكنك إضافة معلمة نوع ضمنية؟ هل تحتاج إلى تغيير سلوك الكتابة السياقية؟ هل تحتاج إلى إضافة شيء خاص إلى التوقيعات؟

ربما يكون الجواب نعم لكل هذه الأشياء. لقد دفعتنا بعيدًا عن ذلك لأن حدسي أخبرني أنه من الأفضل استخدام مفهومين أبسط ومنفصلين والبناء من هناك. الجانب السلبي هو أن هناك القليل من الصيغة المعيارية في بعض الحالات.

إذا كانت هناك مكتبات أحدث تستخدم هذه الأنواع من الأنماط وهذا النموذج المعياري يجعل من الصعب عليهم الكتابة في TypeScript ، فربما ينبغي علينا التفكير في ذلك. ولكن بشكل عام ، تهدف هذه الميزة بشكل أساسي إلى خدمة مستهلكي المكتبات ، لأن موقع الاستخدام هو المكان الذي تحصل فيه على الفوائد هنا على أي حال.

DanielRosenwasser بعد أن بالكاد نزل في حفرة الأرانب. ما زلت لا أجد أي مشاكل في تنفيذ فكرة SaschaNaz ؟ أعتقد أن keysof زائدة عن الحاجة في هذه الحالة. يشير T[p] بالفعل إلى أن p يجب أن يكون أحد العناصر الحرفية لـ T .

كان تفكيري التقريبي في التنفيذ هو تقديم نوع جديد يسمى PropertyReferencedType .

export interface PropertyReferencedType extends Type {
        property: Symbol;
        targetType: ObjectType;
}

عند إدخال دالة معلنة بنوع إرجاع يساوي PropertyReferencedType أو إدخال دالة تشير إلى PropertyReferencedType : سيتم زيادة نوع ElementAccessExpression بخاصية تشير إلى رمز الممتلكات التي تم الوصول إليها.

export interface Type {
        flags: TypeFlags;                // Flags
        /* <strong i="20">@internal</strong> */ id: number;      // Unique ID
        //...
        referencedProperty: Symbol; // referenced property
}

لذا فإن النوع الذي يحتوي على رمز الخاصية المشار إليه يمكن تخصيصه إلى PropertyReferencedType . أثناء التدقيق ، يجب أن يتوافق referencedProperty مع p في T[p] . يجب أيضًا تعيين النوع الأصل لتعبير وصول العنصر إلى T . ولتسهيل الأمور ، يجب أن يكون p ثابتًا أيضًا.

النوع الجديد PropertyReferencedType موجود فقط داخل الوظيفة "كنوع لم يتم حله". في موقع الاتصال ، يتعين على المرء حل النوع بـ p :

interface A { a: string }
declare function getProp(p: string): A[p]
getProp('a'); // string

ينتشر PropertyReferencedType فقط من خلال تعيينات الوظيفة ولا يمكنه الانتشار من خلال تعبيرات الاستدعاء ، لأن PropertyReferencedType هو نوع مؤقت فقط يهدف إلى المساعدة في فحص جسم الوظيفة بنوع الإرجاع T[p] .

إذا قدمت عوامل تشغيل من النوع keysof و T[K] ، فهل هذا يعني أنه يمكننا استخدامها على النحو التالي:

interface A {
  a: number;
  b: string;
}
type AK = keysof A; // "a" | "b"
type AV = A[AK]; // number | string ?
type AA = A["a"]; // number ?
type AB = A["b"]; // string ?
type AC = A["c"]; // error?
type AN = A[number]; // error?

type X1 = keysof { [index: string]: number; }; // string ?
type X2 = keysof { [index: string]: number; [index: number]: string; }; // string | number ?

DanielRosenwasser لن يكون

function foo<T, K extends keysof T>(obj: T, key: K): T[K] {
    // ...
}
// same as ?
function foo<K, V, T extends { [k: K]: V; }>(obj: T, key: K): V {
    // ...
}

لا أرى كيف سيتم كتابة التوقيع لـ Underscore _.pick :

o2 = _.pick(o1, 'p1', 'p2');

pick(Object, ...props: String[]) : WHAT GOES HERE;

rtm لقد اقترحته في https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -234724380. على الرغم من أنه قد يكون من الأفضل فتح مشكلة جديدة ، على الرغم من أنها مرتبطة بهذه المشكلة.

التنفيذ متاح الآن في # 11929.

هل كانت هذه الصفحة مفيدة؟
0 / 5 - 0 التقييمات