Typescript: Saran: Ketik Jenis properti

Dibuat pada 28 Nov 2014  ·  76Komentar  ·  Sumber: microsoft/TypeScript

Motivasi

Banyak pustaka / kerangka kerja / pola JavaScript melibatkan komputasi berdasarkan nama properti suatu objek. Misalnya model Backbone , transformasi fungsional pluck , ImmutableJS semuanya didasarkan pada mekanisme tersebut.

//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'];

Kita dapat dengan mudah memahami dalam contoh-contoh itu hubungan antara api dan batasan tipe yang mendasarinya.
Dalam kasus model tulang punggung, ini hanya semacam _proxy_ untuk objek berjenis:

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

Untuk kasus pluck , ini adalah transformasi

T[] => U[]

dimana U adalah tipe properti dari T prop .

Namun kami tidak memiliki cara untuk mengekspresikan hubungan seperti itu dalam TypeScript, dan berakhir dengan tipe dinamis.

Solusi yang diusulkan

Solusi yang diusulkan adalah memperkenalkan sintaks baru untuk tipe T[prop] mana prop adalah argumen dari fungsi yang menggunakan tipe seperti nilai kembalian atau parameter tipe.
Dengan sintaks tipe baru ini kita dapat menulis definisi berikut:

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][] 

Dengan cara ini, ketika kami menggunakan model Backbone kami, TypeScript dapat dengan benar mengetik-memeriksa panggilan get dan set .

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

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

Konstanta prop

Paksaan

Jelas konstanta harus dari jenis yang dapat digunakan sebagai jenis indeks ( string , number , Symbol ).

Kasus yang dapat diindeks

Mari kita lihat definisi Map :

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

Jika T dapat diindeks, peta kita mewarisi perilaku ini:

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

Sekarang get memiliki untuk tipe get(prop: string): number .

Interogasi

Sekarang ada beberapa kasus di mana saya kesulitan memikirkan perilaku _correct_, mari kita mulai lagi dengan definisi Map .
Jika alih-alih mengirimkan { [index: string]: number } sebagai parameter tipe yang akan kami berikan
{ [index: number]: number } haruskah kompilator memunculkan kesalahan?

jika kita menggunakan pluck dengan ekspresi dinamis untuk prop sebagai ganti konstanta:

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

atau dengan konstanta yang bukan merupakan properti tipe yang diteruskan sebagai parameter.
jika panggilan ke pluck menimbulkan kesalahan karena kompilator tidak dapat menyimpulkan tipe T[prop] , haruskah T[prop] diselesaikan menjadi {} atau any , jika demikian haruskah kompilator dengan --noImplicitAny memunculkan kesalahan?

Fixed Suggestion help wanted

Komentar yang paling membantu

@weswigham @mhegazy , dan saya telah membahas ini baru-baru ini; kami akan memberi tahu Anda perkembangan apa pun yang kami hadapi, dan perlu diingat bahwa ini hanya membuat prototipe ide.

Ide terkini:

  • Operator keysof Foo untuk mengambil gabungan nama properti dari Foo sebagai tipe literal string.
  • Tipe Foo[K] yang menentukan bahwa untuk beberapa tipe K yang merupakan tipe literal string atau gabungan tipe literal string.

Dari blok dasar ini, jika Anda perlu menyimpulkan literal string sebagai jenis yang sesuai, Anda dapat menulis

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

Di sini, K akan menjadi subtipe dari keysof T yang berarti bahwa itu adalah tipe literal string atau gabungan tipe literal string. Apa pun yang Anda berikan untuk parameter key harus diketik secara kontekstual oleh literal / gabungan literal tersebut, dan disimpulkan sebagai tipe literal string tunggal.

Misalnya

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");

Masalah terbesar adalah bahwa keysof sering kali perlu "menunda" operasinya. Terlalu bersemangat dalam cara mengevaluasi tipe, yang merupakan masalah untuk parameter tipe seperti pada contoh pertama yang saya posting (yaitu kasus yang benar-benar ingin kita selesaikan sebenarnya adalah bagian yang sulit: smile :).

Semoga itu memberi Anda semua pembaruan.

Semua 76 komentar

Kemungkinan duplikat # 394

Lihat juga https://github.com/Microsoft/TypeScript/issues/1003#issuecomment -61171048

@NoelAbrahams Saya benar-benar tidak berpikir bahwa ini adalah duplikat # 394, sebaliknya kedua fitur tersebut cukup saling melengkapi seperti:

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

Akan ideal

@tokopedia

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

Apa yang harus dilakukan dalam kasus ini?

Ini kurang lebih kasus yang sama dari yang ada di paragraf terakhir masalah saya. Seperti yang saya katakan kami memiliki pilihan ganda, Kami dapat melaporkan kesalahan, atau menyimpulkan any untuk T[prop] , saya pikir solusi kedua lebih logis

Proposal yang bagus. Setuju, ini akan menjadi fitur yang berguna.

@fdecampredon , saya yakin ini adalah duplikat. Lihat komentar dari Dan dan tanggapan terkait yang berisi saran untuk membertypeof .

IMO semua ini adalah banyak sintaks baru untuk kasus penggunaan yang agak sempit.

@NoelAbrahams itu tidak sama.

  • memberof T mengembalikan tipe yang mana hanya bisa berupa string dengan nama properti yang valid T instance.
  • T[prop] mengembalikan jenis properti T dinamai dengan string yang diwakili oleh prop argumen / variabel.

Ada batasan untuk memberof yang jenis prop parameter harus memberof T .

Sebenarnya, saya ingin memiliki sistem yang lebih kaya untuk jenis inferensi berdasarkan jenis metadata. Tetapi operator semacam itu adalah awal yang baik serta memberof .

Ini menarik dan diinginkan. TypeScript belum bekerja dengan baik dengan kerangka kerja string-heavy dan ini jelas akan banyak membantu.

TypeScript tidak bekerja dengan baik dengan kerangka kerja yang banyak string

Benar. Masih tidak mengubah fakta bahwa ini adalah saran duplikat.

namun dan ini jelas akan sangat membantu [untuk mengetik kerangka kerja string-heavy]

Tidak yakin tentang itu. Tampaknya agak sedikit demi sedikit dan agak spesifik untuk pola proxy-object yang diuraikan di atas. Saya lebih memilih pendekatan yang lebih holistik untuk masalah string ajaib di sepanjang baris # 1003.

1003 menyarankan any sebagai tipe kembalian getter. Proposal ini menambahkan di atas bahwa dengan menambahkan cara untuk mencari jenis nilai juga - campuran akan menghasilkan sesuatu seperti ini:

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

@ spion , maksud Anda # 394? Jika Anda membaca lebih lanjut, Anda akan melihat yang berikut:

Saya memikirkan tentang jenis pengembalian tetapi meninggalkannya agar tidak membuat saran keseluruhan terlalu besar.

Ini adalah pemikiran awal saya tetapi memiliki masalah. Bagaimana jika ada beberapa argumen tipe memberof T , yang mana yang dirujuk oleh membertypeof T ?

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

Ini memecahkan masalah "argumen yang mana yang saya maksud", tetapi nama membertypeof tampaknya salah dan bukan penggemar operator yang menargetkan nama properti.

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

Saya pikir ini bekerja lebih baik.

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

Sayangnya tidak yakin bahwa saya memiliki solusi yang bagus meskipun saya yakin saran terakhir memiliki potensi yang layak.

OK @NoelAbrahams ada komentar di # 394 yang mencoba menggambarkan kurang lebih sama dengan yang ini.
Sekarang saya pikir dari T[prop] mungkin sedikit lebih elegan daripada proposisi berbeda dari komentar ini, dan proposisi dalam terbitan ini mungkin berjalan sedikit lebih jauh dalam refleksi.
Untuk alasan ini saya tidak berpikir itu harus ditutup sebagai duplikat.
Tapi saya kira saya bias karena sayalah yang menulis masalah;).

@fdecampredon , lebih banyak lebih meriah: smiley:

@NoelAbrahams Ups , saya melewatkan bagian itu. Tentu, itu hampir sama (yang ini sepertinya tidak memperkenalkan parameter umum lain, yang mungkin menjadi masalah atau tidak)

Setelah melihat Flow, saya pikir akan lebih elegan dengan sistem tipe yang sedikit lebih kuat dan tipe khusus daripada penyempitan tipe ad hoc.

Yang kami maksud dengan get(prop: string): Contact[prop] misalnya hanyalah serangkaian kemungkinan overloading:

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

// is morally equivalent to 

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

Dengan asumsi keberadaan operator tipe & (tipe persimpangan), ini adalah tipe yang setara dengan

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

Sekarang kita telah menerjemahkan kasus non-umum kita hanya dalam jenis ekspresi tanpa perlakuan khusus (tidak ada [prop] ), kita dapat menjawab pertanyaan tentang parameter.

Idenya adalah untuk menghasilkan tipe ini dari parameter tipe. Kita mungkin mendefinisikan beberapa tipe generik dummy khusus $MapProperties , $Name dan $Value untuk mengekspresikan tipe yang kita hasilkan dalam istilah ekspresi tipe saja (tanpa sintaks khusus) sambil tetap mengisyaratkan pemeriksa tipe bahwa sesuatu harus dilakukan.

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

Ini mungkin tampak rumit dan mendekati tipe templating atau poor's man dependent tetapi tidak dapat dihindari ketika seseorang ingin tipe bergantung pada nilai.

Area lain di mana ini akan berguna adalah melakukan iterasi pada properti objek yang diketik:

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; }

Jenis-jenis ini setidaknya secara teoritis mungkin untuk disimpulkan (terdapat informasi jenis yang cukup untuk menyimpulkan hasil dari pengulangan for ), tetapi mungkin juga cukup berlebihan.

Perhatikan bahwa versi react berikutnya akan sangat diuntungkan dari pendekatan itu, lihat https://github.com/facebook/react/issues/3398

Seperti https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -64944856, ketika string disediakan oleh ekspresi selain string literal, fitur ini akan cepat rusak karena Masalah Halting. Namun, apakah versi dasar ini masih dapat diimplementasikan menggunakan pembelajaran dari masalah Simbol ES6 (# 2012)?

Disetujui.

Kami ingin mencobanya di cabang percobaan untuk mengetahui sintaksnya.

Hanya ingin tahu versi proposal mana yang akan diterapkan? Yaitu T[prop] akan dievaluasi di situs panggilan dan bersemangat diganti dengan tipe konkret atau akan menjadi bentuk baru dari variabel tipe?

Saya pikir kita harus mengandalkan sintaks yang lebih umum dan tidak terlalu bertele-tele seperti yang didefinisikan di # 3779.

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

Atau apakah tidak mungkin untuk menyimpulkan tipe A?

Hanya ingin mengatakan bahwa saya membuat alat codegen kecil untuk membuat integrasi TS dengan ImmutableJS lebih mudah sementara kami menunggu solusi normal: https://www.npmjs.com/package/tsimmutable. Ini cukup sederhana, tetapi saya pikir ini akan berfungsi untuk sebagian besar kasus penggunaan. Mungkin itu akan membantu seseorang.

Juga saya ingin mencatat, bahwa solusi dengan tipe anggota mungkin tidak berfungsi dengan 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 Sesuatu seperti ini dapat bekerja:

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>

Ini tidak akan mengecualikan node DOM atau objek Tanggal tetapi tidak boleh digunakan sama sekali dalam struktur yang tidak dapat diubah. https://github.com/facebook/immutable-js/wiki/Converting-from-JS-objects

Bekerja secara bertahap dari solusi terbaik

Saya pikir akan berguna untuk pertama-tama memulai dengan solusi terbaik yang tersedia saat ini, dan kemudian secara bertahap mengerjakannya dari sana.

Katakanlah kita membutuhkan fungsi yang mengembalikan nilai properti untuk setiap objek non-primitif yang berisi:

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

Sekarang kita ingin nilai yang dikembalikan memiliki tipe properti target, sehingga parameter tipe umum lainnya dapat ditambahkan untuk tipenya:

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

Jadi kasus penggunaan dengan kelas akan terlihat seperti:

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

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

Dan result akan menerima tipe number .

Namun, tampaknya ada referensi duplikat ke 'anggota' dalam panggilan: satu ada di parameter type, dan yang lainnya adalah string _literal_ yang akan selalu menerima representasi string dari nama properti target. Ide dari proposal ini adalah bahwa keduanya dapat disatukan menjadi satu jenis parameter yang hanya akan menerima representasi string.

Jadi perlu diamati di sini bahwa string juga akan bertindak sebagai parameter _generic_, dan perlu diteruskan sebagai literal agar ini berfungsi (kasus lain dapat mengabaikan nilainya secara diam-diam kecuali beberapa bentuk refleksi runtime tersedia). Jadi untuk memperjelas semantik, perlu ada beberapa cara untuk menandakan hubungan antara parameter generik dan string:

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

Dan sekarang panggilannya akan terlihat seperti:

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

Yang secara internal akan memutuskan untuk:

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

Namun karena C berisi turunan dan properti statis bernama member , ekspresi umum yang lebih tinggi T[PName] bersifat ambigu. Karena maksud di sini sebagian besar untuk menerapkannya ke properti instance, solusi seperti operator typeon diusulkan dapat digunakan secara internal, yang juga dapat meningkatkan semantik karena T[PName] ditafsirkan oleh beberapa orang sebagai mewakili a _value reference_, belum tentu tipe:

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

(Ini juga akan berfungsi jika T adalah jenis antarmuka yang kompatibel, karena typeon mendukung antarmuka juga)

Untuk mendapatkan properti statis, fungsi akan dipanggil dengan konstruktor itu sendiri dan tipe konstruktor harus diteruskan sebagai typeof C :

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

(Secara internal typeon (typeof C) diselesaikan menjadi typeof C jadi ini akan berhasil)
result sekarang akan menerima jenis string .

Untuk mendukung tipe tambahan dari pengenal properti, ini bisa ditulis ulang sebagai:

type PropertyIdentifier = string|number|Symbol;

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

Jadi ada beberapa fitur berbeda yang dibutuhkan untuk mendukung ini:

  • Izinkan nilai literal untuk diteruskan sebagai parameter generik.
  • Izinkan literal tersebut untuk menerima nilai argumen fungsi.
  • Izinkan bentuk obat generik yang lebih tinggi.
  • Izinkan referensi jenis properti melalui parameter umum literal pada jenis _generic_ melalui jenis generik yang lebih tinggi.

Memikirkan kembali masalahnya

Sekarang mari kembali ke solusi awal:

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

Ada satu keuntungan dari solusi ini, yaitu dapat dengan mudah diperluas untuk mendukung nama jalur yang lebih kompleks, yang merujuk tidak hanya properti langsung, tetapi juga properti objek bertingkat:

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

    return <P> resultValue;
}

dan sekarang:

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

result akan mendapatkan tipe string[] sini.

Pertanyaannya adalah, haruskah jalur bertingkat juga didukung? Akankah fitur tersebut benar-benar berguna jika tidak ada pelengkapan otomatis yang tersedia saat mengetiknya?

Ini menunjukkan bahwa solusi yang berbeda mungkin dilakukan, yang akan bekerja ke arah lain. Kembali ke solusi asli:

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

Melihat ini dari arah lain, dimungkinkan untuk mengambil referensi tipe itu sendiri dan mengubahnya menjadi string:

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

@typeReferencePathOf memiliki konsep yang mirip dengan nameOf tetapi berlaku untuk parameter umum. Ini akan mengambil referensi jenis yang diterima typeof instance.member (atau alternatif typeon C.member ), mengekstrak path properti member , dan mengubahnya menjadi string literal "member" pada kompilasi waktu. Pelengkap yang kurang terspesialisasi @typeReferenceOf akan menyelesaikan string lengkap "typeof instance.member" .

Jadi sekarang:

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

Akan memutuskan untuk:

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

Dan implementasi serupa untuk getPath :

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

Akan memutuskan untuk:

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

Karena @typeReferencePathOf(P) hanya disetel sebagai nilai default. Argumen dapat dinyatakan secara manual jika diperlukan:

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

Jika parameter tipe kedua tidak akan diberikan @typeReferencePathOf() dapat menghasilkan undefined atau alternatifnya string kosong "" . Jika suatu jenis diberikan tetapi tidak memiliki jalur internal, ini akan menghasilkan "" .

Keuntungan dari pendekatan ini:

  • Tidak membutuhkan obat generik yang lebih tinggi.
  • Tidak perlu mengizinkan literal sebagai parameter umum.
  • Tidak memerlukan perpanjangan obat generik dengan cara apa pun.
  • Mengizinkan pelengkapan otomatis saat mengetik ekspresi typeof dan menggunakan mekanisme pemeriksaan tipe yang ada untuk memvalidasinya alih-alih perlu mengubahnya dari representasi string pada waktu kompilasi.
  • Dapat digunakan dalam kelas generik juga.
  • Tidak membutuhkan typeon (tetapi dapat mendukungnya).
  • Dapat diterapkan dalam skenario lain.

+1

Bagus! Saya sudah mulai menulis proposal besar untuk mengatasi masalah yang sama, tetapi menemukan diskusi ini, jadi mari kita bahas di sini.

Berikut ini kutipan dari masalah saya yang tidak terkirim:

Pertanyaan : Bagaimana fungsi berikut harus dideklarasikan dalam .d.ts agar dapat digunakan dalam konteks di mana seharusnya digunakan?

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

Fungsi ini (dengan sedikit variasi) dapat ditemukan di hampir setiap pustaka "utils" generik .

Ada dua kasus penggunaan berbeda dari mapValues di alam liar:

Sebuah. _Object-as-a- _Dictionary _ di mana nama properti dinamis, nilai memiliki satu jenis yang sama, dan fungsinya dalam kasus umum monomorfik (pahami jenis ini)

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

b. _Object-as-a- _Record _ di mana setiap properti memiliki tipenya sendiri, sedangkan fungsinya polimorfik parametrik (dengan implementasi)

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"

Instrumentasi terbaik yang tersedia untuk mapValues dengan TypeScript saat ini adalah:

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

Tidak sulit untuk melihat bahwa tanda tangan ini sangat cocok dengan kasus penggunaan _Object-as-a- _Dictionary _, sementara menghasilkan 1 hasil yang agak mengejutkan dari jenis inferensi saat diterapkan dalam kasus di mana _Object-as-a-_ _Record _ adalah niatnya.

Jenis {p1: T1, p2: T2, ...pn: Tn} akan dipaksakan ke {[key: string]: T1 | T2 | ... | Tn } menggabungkan semua jenis dan secara efektif membuang semua informasi tentang struktur catatan:

  • yang benar dan diharapkan 2 baik-baik saja console.log(res.a[0].toFixed(2)) akan ditolak oleh compiler;
  • console.log((<number>res['a'][0]).toFixed(2)) diterima memiliki dua tempat yang tidak terkontrol dan rawan kesalahan: nama properti type-cast dan arbitrary.

1, 2 - untuk programmer yang bermigrasi dari ES ke TS


Langkah-langkah yang memungkinkan untuk mengatasi masalah ini (dan menyelesaikan masalah lainnya, juga)

1. Perkenalkan rekaman variadic dengan bantuan tipe literal string

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. Izinkan tipe formal untuk digabungkan dengan tipe formal lainnya yang dibatasi untuk memperpanjang 'string'

contoh:


declare function mapValues<p extends string, T1[p], T2[p]>(
     obj:{...p: T1[p]}, fn:(arg: T1[p]) => T2[p]
): {...p: T2[p]};
3. Izinkan penghancuran tipe formal

contoh:

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

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

dengan ketiga langkah bersama-sama kami bisa menulis

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]> 

Saya tahu bahwa sintaksnya lebih panjang daripada yang diusulkan oleh @fdecampredon
tetapi saya tidak dapat membayangkan bagaimana cara mengungkapkan tipe mapValues atau combineReducers dengan proposal asli.

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

dengan proposal saya deklarasinya akan terlihat seperti:

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] };

Apakah itu bisa diungkapkan dengan proposal asli?

@spion , @fdecampredon ?

@Artazor Jelas peningkatan. Sekarang tampaknya sudah cukup baik untuk mengungkapkan kasus penggunaan patokan saya untuk fitur ini:

Asumsikan user.id dan user.name adalah jenis kolom database yang berisi number dan string yaitu Column<number> dan Column<string>

Tuliskan jenis fungsi select yang membutuhkan:

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

dan mengembalikan Query<{id: number; name: string}>

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

Tidak yakin bagaimana itu akan diperiksa dan disimpulkan. Sepertinya ada masalah, karena tipe T tidak ada. Apakah tipe pengembalian perlu diekspresikan dalam bentuk yang dihancurkan? yaitu

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

@Artazor lupa bertanya, apakah parameter p extends string benar-benar diperlukan?

Bukankah hal seperti ini cukup?

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

yaitu untuk semua tipe variabel T [p] di {...p:Column<T[p]>}

edit: pertanyaan lain, bagaimana ini akan diperiksa kebenarannya?

@spion Saya mengerti sudut pandang Anda - Anda telah salah paham (tapi ini salah saya), dengan T[p] saya maksud adalah hal yang sama sekali berbeda, dan Anda akan melihat bahwa p extends string adalah hal yang sangat penting di sini. Izinkan saya menjelaskan pemikiran saya secara mendalam.

Mari kita pertimbangkan empat kasus penggunaan untuk struktur data: List , Tuple , Dictionary dan Record . Kami akan menyebutnya sebagai _struktur data konsep_ dan mendeskripsikannya dengan properti berikut:

Struktur data konseptualDiakses oleh
Sebuah. posisi
(jumlah)
b. kunci
(tali)
Hitungan
item
1. variabel
(peran yang sama untuk setiap item)
DaftarKamus
2. diperbaiki
(setiap item memainkan peran uniknya sendiri)
TupleMerekam

Dalam JavaScript, keempat casing ini didukung oleh dua bentuk penyimpanan: Array - untuk a1 dan a2 , dan Object - untuk b1 dan b2 .

_Catatan 1 _. Sejujurnya, tidak benar untuk mengatakan bahwa Tuple didukung oleh Array , karena satu-satunya tipe tupel yang "benar" adalah tipe dari objek arguments yang mana bukan Array . Namun demikian, mereka kurang lebih dapat dipertukarkan, terutama dalam konteks penghancuran dan Function.prototype.apply yang membuka pintu untuk meta-program. TypeScript juga memanfaatkan Array untuk memodelkan Tuple.

_Catatan 2 _. Sementara konsep lengkap Kamus dengan kunci sewenang-wenang diperkenalkan beberapa dekade kemudian dalam bentuk ES6 Map , keputusan awal Brendan Eich untuk menggabungkan Record dan Dictionary (dengan kunci string saja) di bawah kap yang sama Object (di mana obj.prop setara dengan obj["prop"] ) menjadi elemen paling kontroversial dari seluruh bahasa (menurut saya).
Ini adalah kutukan dan berkah dari semantik JavaScript. Itu membuat refleksi menjadi sepele dan mendorong programmer bebas beralih antara level _programming_ dan _meta-programming_ dengan biaya mental yang hampir nol (bahkan tanpa menyadarinya!). Saya percaya ini adalah bagian penting dari kesuksesan JavaScript sebagai bahasa skrip.

Sekarang saatnya TypeScript menyediakan cara untuk mengekspresikan tipe untuk semantik aneh ini. Saat kita berpikir pada level _programming_ semuanya baik-baik saja:

Jenis di tingkat pemrogramanDiakses oleh
Sebuah. posisi
(jumlah)
b. kunci
(tali)
Hitungan
item
1. variabel
(peran yang sama untuk setiap item)
T []{[kunci: string]: T}
2. diperbaiki
(setiap item memainkan peran uniknya sendiri)
[T1, T2, ...]{key1: T1, key2: T2, ...}

Namun, saat kita beralih ke level _meta-programming_, maka hal-hal yang telah diperbaiki pada level _programming_ tiba-tiba menjadi variabel! Dan di sini kita tiba-tiba mengenali hubungan antara tupel dan tanda tangan fungsi dan mengusulkan hal-hal seperti # 5453 (Jenis Variadic) yang sebenarnya hanya mencakup sebagian kecil (tapi cukup penting) dari kebutuhan metaprogram - penggabungan tanda tangan dengan memberikan kemampuan untuk merusak struktur formal. parameter menjadi tipe dari rest-args:

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

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

Dalam kasus ini, jika # 6018 juga akan diimplementasikan, kelas Fungsi akan terlihat seperti ini

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> 
}

Ini bagus tapi tetap tidak lengkap.

Misalnya, bayangkan kita ingin menetapkan tipe yang benar ke fungsi berikut:

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

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

Dengan jenis variadic yang diusulkan, kita tidak dapat mengubah jenis tanda tangan. Di sini kita membutuhkan sesuatu yang lebih kuat. Faktanya, diinginkan untuk memiliki fungsi waktu kompilasi yang dapat beroperasi di atas tipe (seperti dalam Aliran Facebook). Padahal, saya yakin ini hanya mungkin ketika (dan jika) sistem tipe TypeScript akan cukup stabil untuk mengeksposnya ke pemrogram akhir tanpa risiko signifikan untuk merusak lahan pengguna pada pembaruan kecil berikutnya. Oleh karena itu, kami membutuhkan sesuatu yang tidak seradikal dukungan penuh meta-pemrograman tetapi masih mampu menangani masalah yang ditunjukkan.

Untuk mengatasi masalah ini, saya ingin memperkenalkan pengertian _object signature_. Secara kasar itu baik

  1. kumpulan nama properti objek, atau
  2. kumpulan tombol kamus, atau
  3. kumpulan indeks numerik dari sebuah larik
  4. himpunan posisi numerik tupel.

Idealnya, alangkah baiknya memiliki tipe literal integer serta tipe literal string untuk mewakili tanda tangan dari array atau tupel seperti

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

aturan untuk literal integer kongruen dengan string literal;

Kemudian kita bisa memperkenalkan sintaks abstraksi tanda tangan, yang sama persis dengan sintaks objek rest / spread:

{...Props: T }

dengan satu pengecualian penting: di sini Props adalah pengenal yang terikat di bawah pembilang universal:

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

dengan demikian, ia diperkenalkan dalam type lexical scope dan dapat digunakan dimanapun dalam scope ini sebagai nama dari type tersebut. Namun, penggunaannya ganda: ketika digunakan sebagai pengganti nama properti istirahat / sebar itu mewakili abstraksi atas tanda tangan objek ketika digunakan tipe mandiri itu mewakili subtipe dari string (atau nomor masing-masing).

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

Dan inilah bagian yang paling canggih: _Key Dependent Types_

kami memperkenalkan konstruksi tipe khusus: T for Prop (Saya ingin menggunakan sintaks ini daripada T [Prop] yang membingungkan Anda) di mana Prop adalah nama variabel tipe yang menyimpan abstraksi tanda tangan objek. Misalnya <Prop extends string, T for Prop> memperkenalkan dua tipe formal ke dalam jenis cakupan leksikal, Prop dan T dimana diketahui bahwa untuk setiap nilai p dari Prop akan ada jenisnya sendiri T .

Kami tidak mengatakan bahwa di suatu tempat ada objek yang memiliki properti Props dan tipenya adalah T ! Kami hanya memperkenalkan ketergantungan fungsional antara dua jenis. Tipe T berkorelasi dengan anggota tipe Props, dan itu saja!

Ini memberi kita kemungkinan untuk menulis hal-hal seperti

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}>

namun parameter kedua diperlakukan bukan sebagai objek melainkan sebagai peta abstrak pengenal jenis. Dalam perspektif ini T for P dapat digunakan untuk merepresentasikan urutan abstrak dari tipe untuk tupel ketika P adalah subtipe dari bilangan ( @JsonFreeman ?)

Ketika T digunakan di suatu tempat di dalam {...P: .... T .... } itu mewakili tepat satu tipe tertentu dari peta itu.

Itu adalah ide utamaku.
Menunggu pertanyaan, pemikiran, kritik -)

Benar, jadi extends string adalah memperhitungkan array (dan argumen variadic) dalam satu kasus, dan konstanta string (sebagai tipe) di kasus lain. Manis!

Kami tidak mengatakan bahwa suatu tempat adalah objek yang memiliki properti Props dan tipenya adalah T! Kami hanya memperkenalkan ketergantungan fungsional antara dua jenis. Tipe T berkorelasi dengan anggota tipe Props, dan itu saja!

Saya tidak bermaksud begitu, saya lebih serius karena tipenya adalah T [p], kamus tipe yang diindeks oleh p. Jika itu intuisi yang bagus, saya akan menyimpannya.

Secara keseluruhan, sintaksnya mungkin perlu lebih banyak pekerjaan, tetapi ide umumnya terlihat luar biasa.

Apakah mungkin untuk menulis unwrap untuk argumen variadic?

edit: nevermind, saya baru menyadari bahwa ekstensi yang Anda usulkan ke jenis variadic mengatasi hal ini.

Halo semua,
Saya bingung bagaimana menyelesaikan salah satu masalah saya dan menemukan diskusi ini.
Masalahnya adalah:
Saya memiliki metode RpcManager.call(command:Command):Promise<T> , dan penggunaannya akan seperti ini:

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

Solusi yang menurut saya bisa seperti:

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> {
    }
}

Ada pemikiran tentang ini?

@ antanas-arvasevicius blok kode terakhir dalam contoh itu harus melakukan apa yang Anda inginkan.

sepertinya Anda memiliki lebih banyak pertanyaan tentang bagaimana menyelesaikan tugas tertentu; silakan gunakan Stack Overflow atau ajukan masalah jika Anda merasa telah menemukan bug kompilator.

Hai, Ryan, terima kasih atas tanggapan Anda.
Saya sudah mencoba blok kode terakhir itu tetapi tidak berhasil.

Demo cepat:

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

Maaf saya tidak menggunakan Stack Overflow, tapi saya pikir itu terkait dengan masalah ini :)
Haruskah saya membuka edisi baru tentang topik ini?

Parameter tipe generik yang tidak digunakan mencegah inferensi bekerja; secara umum Anda harus _never_ memiliki parameter tipe yang tidak digunakan dalam deklarasi tipe karena tidak ada artinya. Jika Anda mengonsumsi T , semuanya berfungsi:

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);

Itu luar biasa! Terima kasih!
Saya tidak berpikir bahwa parameter tipe dapat ditempatkan di dalam tipe generik dan
TS akan mengekstraknya.
Pada 22 Feb 2016 23:56, [email protected] menulis "Ryan Cavanaugh":

Parameter tipe generik yang tidak digunakan mencegah inferensi bekerja; di
umum Anda harus _never_ memiliki parameter tipe yang tidak terpakai dalam tipe
deklarasi karena tidak ada artinya. Jika Anda mengkonsumsi T, semuanya adil
karya:

Antarmuka Perintah{foo: T} kelas MyCommand mengimplementasikan Perintah <{status: string}> {foo: {status: string; }} kelas RPC {panggilan statis(perintah: Perintah): T {kembali; }}
let response = RPC.call (new MyCommand ());
console.log (response.status);

-
Balas email ini secara langsung atau lihat di GitHub
https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -187404245
.

@ antanas-arvasevicius jika Anda membuat API gaya RPC Saya punya beberapa dokumen yang mungkin berguna bagi Anda: https://github.com/alm-tools/alm/blob/master/docs/contributing/ASYNC.md : rose:

Pendekatan di atas tampak:

  • cukup rumit;
  • jangan menangani penggunaan string dalam kode. string bukan "Temukan semua referensi" -able, atau refactorable (mis. Mengganti nama).
  • beberapa hanya mendukung properti "tingkat pertama" dan bukan ekspresi yang lebih kompleks (yang didukung oleh beberapa kerangka kerja).

Inilah ide lain, terinspirasi oleh pohon C # Expression. Ini hanya ide kasar, tidak ada yang dipikirkan sepenuhnya! Sintaksnya buruk. Saya hanya ingin melihat apakah ini menginspirasi seseorang.

Asumsikan kita memiliki jenis string khusus untuk menunjukkan ekspresi.
Sebut saja type Expr<T, U> = string .
Di mana T adalah tipe objek awal dan U adalah tipe hasil.

Asumsikan kita bisa membuat instance Expr<T,U> dengan menggunakan lambda yang mengambil satu parameter tipe T dan melakukan akses anggota padanya.
Misalnya: person => person.address.city .
Ketika ini terjadi, seluruh lambda dikompilasi menjadi string yang berisi akses apa pun pada parameter itu, dalam hal ini: "address.city" .

Anda dapat menggunakan string biasa sebagai gantinya, yang akan terlihat sebagai Expr<any, any> .

Memiliki tipe Expr dalam bahasa memungkinkan hal-hal seperti itu:

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");

Ini pada dasarnya adalah bentuk terbatas dari apa Ekspresi digunakan dalam C #.
Menurut Anda, apakah ini bisa diperbaiki dan mengarahkan Anda ke suatu tempat yang menarik?

@fdecampredon @RyanCavanaugh

_ ( @ jods4 - Maaf saya tidak membalas saran Anda di sini. Saya harap saran Anda tidak 'terkubur' melalui komentar) _

Saya rasa penamaan fitur ini ('Type Property type') sangat membingungkan dan sangat sulit untuk dipahami. _Sangat_ sulit bahkan untuk mengetahui konsep apa yang dijelaskan di sini dan apa artinya sejak awal!

Pertama, tidak semua tipe memiliki properti! undefined dan null tidak (meskipun ini hanya tambahan terbaru untuk sistem tipe). Primitif seperti number , string , boolean jarang diindeks oleh sebuah properti (mis. 2 ["prop"]? Meskipun ini tampaknya berhasil, hampir selalu kesalahan)

Saya akan menyarankan penamaan masalah ini Referensi jenis properti antarmuka melalui nilai literal string . Topiknya di sini bukan tentang memperkenalkan 'type' baru, tetapi cara yang sangat khusus untuk mereferensikan yang sudah ada menggunakan variabel string atau parameter fungsi yang nilainya _ harus diketahui pada waktu kompilasi_.

Akan sangat bermanfaat jika ini dideskripsikan dan dicontohkan sesederhana mungkin, di luar konteks kasus penggunaan tertentu:

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: Sepertinya untuk mengimplementasikan ini dengan benar compiler harus mengetahui dengan pasti bahwa string yang ditetapkan ke variabel tidak berubah antara titik yang diinisialisasi dan titik yang direferensikan dalam ekspresi tipe. Saya tidak berpikir ini sangat mudah? Apakah asumsi tentang perilaku run-time ini akan selalu berlaku? _

_Edit 2: Mungkin ini hanya akan berfungsi dengan const saat digunakan dengan variabel? _

Saya tidak yakin apakah maksud aslinya adalah hanya mengizinkan referensi properti pada kasus tertentu dari string literal yang diteruskan ke parameter fungsi atau metode? misalnya

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

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

Sudahkah saya menggeneralisasi ini melampaui apa yang semula dimaksudkan?

Bagaimana ini menangani kasus di mana argumen fungsi bukan string literal, yaitu tidak dikenal pada waktu kompilasi? misalnya

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

Apakah akan error, atau default ke any mungkin?

(PS: Jika ini memang niatnya, maka saya akan menyarankan untuk mengganti nama masalah ini menjadi referensi tipe properti Antarmuka dengan fungsi literal string atau argumen metode - Selama ini ternyata, ini jauh lebih akurat dan menjelaskan daripada judul saat ini.)

Berikut adalah contoh benchmark sederhana (cukup untuk ilustrasi) yang menunjukkan apa yang perlu diaktifkan oleh fitur ini:

Tuliskan jenis fungsi ini, yang:

  • mengambil objek yang berisi bidang janji, dan
  • mengembalikan objek yang sama tetapi dengan semua bidang diselesaikan, setiap bidang memiliki tipe yang tepat.
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)
}

Saat dipanggil pada objek berikut:

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

Hasil res harus bertipe {a: number; b: string}


Dengan fitur ini (sebagaimana disempurnakan sepenuhnya oleh @Artazor), tandanya adalah

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

edit: memperbaiki jenis pengembalian di atas, hilang Promise<...> ^^

@pion

Terima kasih telah mencoba memberikan contoh, tetapi tidak ada di sini saya telah menemukan spesifikasi yang masuk akal dan ringkas dan serangkaian contoh yang dikurangi jelas yang _detached_ dari aplikasi praktis dan mencoba menyoroti semantik dan berbagai kasus tepi tentang bagaimana ini akan bekerja, bahkan sesuatu yang mendasar seperti:

function func<T extends object>(name: string): T[name] {
 ...
}
  1. Ada sedikit upaya untuk memberikan judul yang efektif dan jelas (seperti yang saya sebutkan, 'Jenis Jenis Properti' membingungkan dan tidak terlalu menjelaskan nama untuk fitur ini, yang sebenarnya bukan tentang jenis tertentu tetapi tentang referensi jenis). Judul terbaik saya yang disarankan adalah referensi jenis properti _Interface dengan fungsi literal string atau argumen metode_ (dengan asumsi saya tidak salah memahami ruang lingkup ini).
  2. Belum ada penyebutan yang jelas tentang apa yang akan terjadi jika name tidak diketahui pada waktu kompilasi, null, atau tidak ditentukan, misalnya func<MyType>(getRandomString()) , func<MyType>(undefined) .
  3. Belum ada penyebutan yang jelas tentang apa yang akan terjadi jika T adalah tipe primitif seperti number atau null .
  4. Tidak ada detail yang jelas diberikan tentang apakah ini hanya berlaku untuk fungsi dan apakah T[name] dapat digunakan di badan fungsi. Dan dalam kasus ini, apa yang terjadi jika name dipindahkan ke badan fungsi dan dengan demikian nilainya mungkin tidak lagi diketahui pada waktu kompilasi?
  5. Tidak ada spesifikasi sebenarnya dari sintaks: mis. Apakah T["propName"] atau T[propName] bekerja dengan baik dengan sendirinya? tanpa referensi ke parameter atau variabel, maksud saya - ini bisa berguna!
  6. Tidak disebutkan atau diskusi jika T[name] dapat digunakan di parameter lain atau bahkan di luar cakupan fungsi, misalnya
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. Tidak ada diskusi nyata tentang solusi yang relatif sederhana menggunakan parameter generik tambahan dan typeof (meskipun telah disebutkan, tetapi relatif terlambat setelah fitur telah diterima):

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'

Kesimpulan: Tidak ada proposal nyata di sini, hanya satu set demonstrasi use case. Saya terkejut ini telah diterima atau bahkan mendapat minat positif sama sekali oleh tim TypeScript karena saya tidak percaya ini akan sesuai dengan standar mereka. Saya minta maaf jika saya mungkin terdengar sedikit kasar di sini (bukan tipikal saya) tetapi tidak ada kritik saya yang bersifat pribadi atau ditujukan kepada individu tertentu.

Apakah kita benar-benar ingin melangkah sejauh itu dalam hal meta-pemrograman?

JS adalah bahasa dinamis, kode dapat memanipulasi objek dengan cara yang sewenang-wenang.
Ada batasan untuk apa yang dapat kita modelkan dalam sistem tipe dan mudah untuk menghasilkan fungsi yang tidak dapat Anda modelkan di TS.
Pertanyaannya adalah: seberapa jauh hal itu masuk akal dan berguna untuk dilakukan?

Contoh terakhir ( awaitObject ) dari @spion bersamaan:

  • _Sangat kompleks_ dari sudut pandang bahasa (Saya setuju dengan @malibuzios di sini).
  • _Sangat sempit_ dalam hal penerapan. Contoh itu memilih kendala tertentu dan tidak menggeneralisasi dengan baik.

BTW @spion Anda tidak mendapatkan jenis pengembalian dari contoh Anda dengan benar ... Ini mengembalikan Promise .

Kami jauh dari masalah asli, yaitu tentang mengetik API yang mengambil string yang mewakili bidang, seperti _.pluck
API tersebut _harus_ didukung karena umum dan tidak terlalu rumit. Kami tidak membutuhkan model meta seperti { ...p: T[p] } untuk itu.
Ada beberapa contoh di OP, ada lagi dari Aurelia di komentar saya di atas nama terbitan .
Pendekatan yang berbeda dapat mencakup kasus penggunaan tersebut, lihat komentar saya di atas untuk ide yang mungkin.

@ jods4 tidak sempit sama sekali. Ini dapat digunakan untuk sejumlah hal yang tidak mungkin dimodelkan dalam sistem tipe saat ini. Saya akan mengatakan itu salah satu bagian besar terakhir yang hilang dalam sistem tipe TS. Ada banyak contoh dunia nyata di atas oleh @Artazor https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -177287714 hal yang lebih sederhana di atas, contoh "pilih" pembuat kueri sql dan seterusnya.

Ini awaitObject di bluebird. Ini metode nyata. Jenisnya saat ini tidak berguna.

Solusi yang lebih umum akan lebih baik. Ini akan bekerja untuk kasus sederhana dan kompleks. Solusi yang kurang bertenaga akan ditemukan kurang untuk setengah dari kasus dan akan dengan mudah menyebabkan masalah kompatibilitas jika perlu untuk mengadopsi yang lebih umum di masa mendatang.

Ya, ini membutuhkan lebih banyak pekerjaan, tetapi saya juga berpikir @Artazor melakukan pekerjaan yang sangat baik dalam menganalisis dan mengeksplorasi semua aspek yang belum pernah kita pikirkan sebelumnya. Saya tidak akan tinggal, kami telah menyimpang dari masalah aslinya, kami hanya memiliki pemahaman yang lebih baik tentang itu.

Kami mungkin membutuhkan nama yang lebih baik untuk itu, saya akan mencoba tetapi saya biasanya tidak bisa melakukannya dengan benar. Bagaimana dengan "Object generics"?

@spion , hanya untuk

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

(Anda telah melewatkan janji dalam tanda tangan keluaran)
-)

@ jods4 , saya setuju dengan @spion
Ada banyak masalah dunia nyata di mana { ...p: T[p] } adalah solusinya. Untuk menyebutkan satu: bereaksi + fluks / reduks

@spion @Artazor
Saya hanya khawatir hal ini menjadi sangat rumit untuk ditentukan secara tepat.

Motivasi di balik masalah ini telah melayang. Awalnya ini tentang mendukung API yang menerima string untuk menunjukkan bidang objek. Sekarang sebagian besar membahas API yang menggunakan objek sebagai peta atau rekaman dengan cara yang sangat dinamis. Perhatikan bahwa jika kita bisa membunuh dua burung dengan satu batu, saya setuju.

Jika kita kembali ke API yang mengambil masalah string, T[p] bukanlah solusi lengkap menurut saya (saya katakan mengapa sebelumnya).

_Hanya untuk kebenaran_ awaitObject juga harus menerima properti non-Promise, setidaknya Bluebird props tidak. Jadi sekarang kami memiliki:

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

Saya mengubah jenis pengembalian menjadi Promise<T> karena saya mengharapkan notasi ini berfungsi.
Ada kelebihan lain, misalnya kelebihan yang mengambil Promise untuk objek seperti itu (tanda tangannya bahkan lebih menyenangkan). Jadi itu berarti notasi { ...p } perlu dianggap kecocokan yang lebih buruk daripada jenis lainnya.

Menentukan semua ini akan menjadi kerja keras. Saya akan mengatakan ini adalah langkah selanjutnya jika Anda ingin mendorong ini ke depan.

@spion @ jods4

Saya ingin menyebutkan bahwa fitur ini bukan tentang generik dan bukan tentang jenis polimorfik yang lebih tinggi. Ini hanyalah sintaks untuk mereferensikan tipe properti melalui tipe yang memuatnya (dengan beberapa ekstensi "lanjutan" yang dijelaskan di bawah), yang tidak jauh dari konsep typeof . Contoh:

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

Sekarang bandingkan dengan:

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

let y: typeof x.abcd;

Ada dua perbedaan utama dengan typeof . Tidak seperti typeof , ini berlaku untuk tipe (setidaknya yang non-primitif, fakta yang sering dihilangkan di sini) daripada contoh. Selain itu (dan ini adalah hal utama) itu diperluas untuk mendukung penggunaan konstanta string literal (yang harus diketahui pada waktu kompilasi) sebagai deskriptor jalur properti, dan generik:

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

Namun secara teknis typeof dapat diperpanjang untuk mendukung literal string juga (ini termasuk kasus di mana x memiliki tipe generik):

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

Dan dengan ekstensi ini, ini juga dapat digunakan untuk menyelesaikan beberapa kasus target di sini:

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

typeof namun lebih kuat karena mendukung jumlah level bersarang yang tidak terbatas:

let y: typeof x.prop.childProp.deeperChildProp

Dan yang ini hanya berjalan satu tingkat. Yaitu tidak direncanakan (sejauh yang saya tahu) untuk mendukung:

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

Saya pikir ruang lingkup proposal ini (bahkan jika ini bisa disebut proposal pada tingkat ketidakjelasan) terlalu sempit. Ini dapat membantu memecahkan masalah tertentu (meskipun mungkin ada solusi alternatif), yang tampaknya membuat banyak orang sangat ingin mempromosikannya. Namun itu juga memakan sintaks yang berharga dari bahasa tersebut. Tanpa rencana yang lebih luas dan pendekatan berorientasi desain, tampaknya tidak bijaksana untuk terburu-buru memperkenalkan sesuatu yang sampai saat ini bahkan tidak memiliki spesifikasi yang jelas.

_Edits: mengoreksi beberapa kesalahan yang jelas dalam contoh kode_

Saya telah meneliti sedikit tentang alternatif typeof :

Dukungan bahasa masa depan untuk typeof x["abcd"] dan typeof x[42] telah disetujui , dan sekarang berada di bawah # 6606, yang saat ini sedang dalam pengembangan (ada implementasi yang berfungsi).

Ini setengah jalan. Dengan ini, sisanya dapat dilakukan dalam beberapa tahap:

(1) Tambahkan dukungan untuk konstanta literal string (atau bahkan konstanta numerik - mungkin berguna untuk tupel?) Sebagai penentu properti dalam ekspresi tipe, misalnya:

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

(2) Izinkan menerapkan penentu ini ke tipe generik

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

(3) Izinkan penentu ini untuk diteruskan, dan dalam praktiknya "instantiate" referensi, melalui argumen fungsi ( typeof list[0] direncanakan jadi saya yakin ini dapat mencakup kasus yang lebih kompleks seperti 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 ketik di sini adalah yang diusulkan di # 1809)

Meskipun lebih kuat (mis. Mungkin mendukung typeof obj[propName][nestedPropName] ) ada kemungkinan pendekatan alternatif ini mungkin tidak mencakup semua kasus yang dijelaskan di sini. Tolong beri tahu saya jika ada contoh yang sepertinya tidak akan ditangani oleh ini (satu skenario yang muncul di pikiran adalah ketika contoh objek tidak dilewatkan sama sekali, namun, sulit bagi saya pada saat ini membayangkan kasus di mana yang akan dibutuhkan, meskipun saya rasa itu mungkin).

_Edits: mengoreksi beberapa kesalahan dalam kode_

@bayu_joo
Beberapa pemikiran:

  • Ini memperbaiki definisi API tetapi tidak membantu pemanggil. Kami masih membutuhkan sejenis nameof atau Expression di situs panggilan.
  • Ia bekerja untuk definisi .d.ts , tetapi umumnya tidak akan dapat disangkal secara statis atau bahkan dapat diverifikasi dalam fungsi / implementasi TS.

Semakin saya memikirkannya, semakin banyak Expression<T, U> tampak seperti solusi untuk jenis API tersebut. Ini memperbaiki masalah situs panggilan, pengetikan hasil dan dapat disimpulkan + diverifikasi dalam implementasi.

@ jodie.semarang

Anda menyebutkan dalam komentar sebelumnya di atas bahwa pluck dapat diimplementasikan dengan ekspresi. Meskipun saya dulu sangat akrab dengan C #, saya tidak pernah benar-benar bereksperimen dengan mereka, jadi saya akui saya tidak benar-benar memiliki pemahaman yang baik tentang mereka saat ini.

Bagaimanapun, hanya ingin menyebutkan TypeScript mendukung _type argumen inference_ , jadi alih-alih menggunakan pluck , seseorang dapat menggunakan map dan mencapai hasil yang sama, yang bahkan tidak memerlukan menentukan parameter generik ( itu disimpulkan) dan juga akan memberikan pemeriksaan tipe lengkap dan penyelesaian:

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[]' 

Dimana map (sangat disederhanakan untuk demonstrasi) dideklarasikan sebagai

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

Pola inferensi yang sama ini dapat digunakan sebagai 'trik' untuk meneruskan hasil apa pun dari lambda arbitrer (yang bahkan tidak perlu dipanggil) ke suatu fungsi dan menyetel jenis kembaliannya:

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: mungkin terlihat konyol untuk menyebarkannya dengan lambda di sini dan bukan hanya benda itu sendiri! namun dalam kasus yang lebih kompleks seperti objek bersarang (misalnya x.prop.Childprop ) mungkin ada referensi yang tidak terdefinisi atau null yang mungkin error. Memiliki itu dalam lambda, yang tidak selalu disebut, menghindari itu._

Saya akui, saya tidak terlalu paham dengan beberapa kasus penggunaan yang dibahas di sini. Secara pribadi saya tidak pernah merasa perlu memanggil fungsi yang menggunakan nama properti sebagai string. Meneruskan lambda (di mana kelebihan yang Anda gambarkan juga berlaku) dalam kombinasi dengan antarmuka argumen tipe biasanya cukup untuk menyelesaikan banyak masalah umum.

Alasan saya menyarankan pendekatan alternatif typeof sebagian besar karena hal itu tampaknya mencakup kasus penggunaan yang sama dan menyediakan fungsionalitas yang sama untuk apa yang dijelaskan orang di sini. Apakah saya akan menggunakannya sendiri? Saya tidak tahu, tidak pernah merasakan kebutuhan nyata untuk (bisa jadi saya hampir tidak pernah menggunakan perpustakaan eksternal seperti garis bawah, saya hampir selalu mengembangkan fungsi utilitas sendiri, berdasarkan kebutuhan).

@malibuzios Benar, pluck agak konyol dengan lambda + map sekarang sudah ada di dalamnya .

Dalam komentar ini saya memberikan 5 API berbeda yang semuanya mengambil string - semuanya terhubung untuk mengamati perubahan.

@ jods4 dan lainnya

Hanya ingin menyebutkan bahwa ketika # 6606 selesai, dengan hanya mengimplementasikan tahap 1 (izinkan literal string konstan untuk digunakan sebagai penentu properti) beberapa fungsi yang diperlukan di sini dapat dicapai, meskipun tidak seelegan mungkin (mungkin memerlukan nama properti yang akan dideklarasikan sebagai konstanta, menambahkan pernyataan tambahan).

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);

Namun, jumlah upaya untuk menerapkan ini mungkin jauh lebih rendah daripada menerapkan tahap 2 dan 3 juga (saya tidak yakin tetapi mungkin 1 sudah tercakup oleh # 6606). Dengan penerapan tahap 2, ini juga akan mencakup kasus di mana x memiliki tipe generik (tapi saya tidak yakin apakah itu benar-benar diperlukan?).

Sunting: Alasan saya menggunakan konstanta eksternal dan tidak hanya menulis nama properti dua kali bukan hanya untuk mengurangi pengetikan, tetapi juga untuk memastikan nama selalu cocok, meskipun ini masih tidak dapat digunakan dengan alat seperti "ganti nama" dan "temukan semua referensi ", yang menurut saya sangat merugikan.

@ jods4 Saya masih mencari solusi yang lebih baik yang tidak menggunakan string. Saya akan mencoba memikirkan apa yang bisa dilakukan dengan nameof dan variannya.

Inilah ide lain.

Karena literal string sudah didukung sebagai tipe, misalnya seseorang sudah dapat menulis:

let x: "HELLO"; 

Seseorang dapat melihat string literal yang diteruskan ke fungsi sebagai bentuk spesialisasi umum

(_Edit: contoh-contoh ini telah diperbaiki untuk memastikan bahwa s tidak dapat diubah dalam badan fungsi, meskipun const tidak didukung pada posisi parameter pada saat ini (tidak yakin tentang readonly meskipun) ._):

function func(const s: string)

Tipe parameter string diasosiasikan dengan s dapat dilihat sebagai generik implisit di sini (karena dapat dikhususkan untuk string literal). Untuk kejelasan, saya akan menulisnya lebih eksplisit (meskipun saya tidak yakin apakah memang perlu):

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

secara internal dapat mengkhususkan diri untuk:

function func(s: "TEST")

Dan sekarang ini dapat digunakan sebagai cara alternatif untuk "meneruskan" nama properti, yang menurut saya menangkap semantik lebih baik di sini.

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

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

observeProperty(x, nameof(x.age))

Karena dalam T[S] , T , dan S adalah parameter umum. Tampaknya lebih alami untuk memiliki dua tipe yang digabungkan dalam posisi tipe daripada mencampur elemen run-time dan tipe lingkup (misalnya T[someString] ).

_Edits: contoh yang ditulis ulang untuk memiliki tipe parameter string yang tidak dapat diubah._

Karena saya menggunakan TS 1.8.7 dan bukan versi terbaru, saya tidak menyadarinya di versi yang lebih baru di

const x = "Hello";

Jenis x sudah disimpulkan sebagai jenis literal "Hello" , (yaitu: x: "Hello" ) yang sangat masuk akal (lihat # 6554).

Jadi wajar jika ada cara untuk mendefinisikan parameter sebagai const (atau mungkin readonly juga akan berfungsi?):

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

Maka saya yakin ini bisa bertahan:

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

Karena beberapa di antaranya agak baru dan bergantung pada fitur bahasa terkini, saya akan mencoba meringkasnya sejelas mungkin:

(1) Ketika variabel string const (dan mungkin readonly juga?) Menerima nilai yang diketahui pada waktu kompilasi, variabel secara otomatis menerima tipe literal yang memiliki nilai yang sama ( Saya yakin ini adalah perilaku baru yang tidak terjadi pada 1.8.x ), mis

const x = "ABCD";

Jenis x disimpulkan menjadi "ABCD" , bukan string !, Misalnya seseorang dapat menyatakan ini sebagai x: "ABCD" .

(2) Jika readonly parameter fungsi diizinkan, parameter readonly dengan tipe generik secara alami akan mengkhususkan tipenya ke literal string ketika diterima sebagai argumen, karena parameternya tidak dapat diubah!

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

Di sini S telah diselesaikan ke tipe "ABCD" , bukan string !

Namun, jika parameter str tidak dapat diubah, kompilator tidak dapat menjamin bahwa ia tidak ditempatkan kembali dalam tubuh fungsi, dengan demikian, tipe yang disimpulkan hanya string .

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

func("ABCD");

(3) Hal ini dimungkinkan untuk memanfaatkan ini dan mengubah proposal asli agar penentu properti menjadi referensi ke suatu jenis , yang akan dibatasi menjadi turunan dari string (dalam beberapa kasus hal itu bahkan mungkin sesuai untuk membatasinya hanya untuk tipe literal tunggal, meskipun saat ini tidak ada cara untuk melakukannya), daripada entitas run-time:

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

Memanggil ini tidak memerlukan secara eksplisit menentukan argumen tipe apa pun, karena TypeScript mendukung inferensi argumen tipe:

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: mengoreksi beberapa kesalahan ejaan dan kode._
_Catatan: versi umum dan perluasan dari pendekatan yang dimodifikasi ini sekarang juga dilacak di # 7730._

Berikut adalah penggunaan lain dari tipe tipe properti (atau "generik terindeks" seperti yang saya sering sebut akhir-akhir ini) yang muncul dalam diskusi dengan @Raynos

Kita sudah bisa menulis pemeriksa umum berikut untuk array:

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
    }
}

Setelah kami mengindeks obat generik, kami juga dapat menulis pemeriksa umum untuk objek:

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;
  }
}

yang bersama dengan checker primitif untuk string, number, boolean, null dan undefined akan memungkinkan Anda menulis hal-hal seperti ini:

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

dan dapatkan hasilnya dengan pemeriksa waktu proses yang tepat _ dan_ kompilasi penjaga tipe waktu, pada saat yang sama :)

Adakah yang pernah melakukan pekerjaan ini, atau apakah itu ada di radar siapa pun? Ini akan menjadi peningkatan besar pada berapa banyak pustaka standar yang dapat menggunakan pengetikan, termasuk yang seperti lodash atau ramda dan banyak antarmuka database.

@malibuzios Saya yakin Anda mendekati saran @Artazor :)

Untuk mengatasi masalah Anda:

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

Ini akan menjadi

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

Dengan cara ini, nama akan dikunci ke tipe yang paling spesifik (tipe konstan string) saat dipanggil dengan:

func("test")

Jenis S menjadi "test" (bukan string ). Karena itu str tidak dapat diberikan nilai yang berbeda. Jika Anda mencobanya, misalnya

str = "other"

Kompilator dapat menghasilkan kesalahan (kecuali jika ada masalah kesehatan varians;))

Alih-alih hanya mendapatkan satu tipe properti, saya ingin memiliki opsi untuk mendapatkan tipe super sembarang.

Jadi saran saya adalah menambahkan sintaks berikut: Daripada hanya memiliki T[prop] , saya ingin menambahkan sintaks T[...props] . Dimana props harus berupa array anggota T . Dan tipe yang dihasilkan adalah tipe super T dengan anggota T ditentukan dalam props .

Saya pikir ini akan sangat berguna di Sequelize - ORM populer untuk node.js. Untuk alasan keamanan dan untuk alasan kinerja, sebaiknya hanya menanyakan atribut dalam tabel yang perlu Anda gunakan. Itu sering kali berarti tipe super dari suatu tipe.

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

(Dalam contoh saya ini termasuk operator memberof , yang Anda harapkan, dan juga ekspresi options.attributes yang sama dengan typeof options.attributes , tapi menurut saya typeof operator berlebihan dalam kasus ini, karena berada dalam posisi yang mengharapkan tipe.).

Jika tidak ada yang bersikeras, saya mulai mengerjakan ini.

Apa pendapat Anda tentang keamanan tipe di dalam fungsi, yaitu memastikan pernyataan pengembalian mengembalikan sesuatu yang dapat dialihkan ke tipe pengembalian?

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] ?
}

Juga nama "Jenis Properti" yang digunakan di sini sepertinya agak salah. Ini semacam menyatakan bahwa tipe memiliki properti, yang dimiliki hampir semua tipe.

Bagaimana dengan "Jenis yang Dirujuk Properti"?

Apa pendapat Anda tentang keamanan tipe di dalam fungsi

Brainstorming saya:

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
}

Dan larang string normal di jalur pengembalian.

Masalah @malibuzios dengan ide Anda adalah bahwa hal itu memerlukan spesifikasi semua generik jika tidak mungkin untuk menyimpulkan, yang dapat menjadi viral / mencemari basis kode.

Ada komentar dari tim TS tentang https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -239653337?

@RyanCavanaugh @mhegazy dll?

Mengenai namanya, saya mulai menyebut fitur ini (setidaknya bentuk yang diusulkan @Artazor ) "Generik Terindeks"

Solusi dari sudut pandang lain mungkin untuk masalah ini. Saya tidak yakin apakah itu sudah dibawa, itu utas yang panjang. Mengembangkan saran umum string, kita bisa memperluas tanda tangan indeksasi. Karena literal string dapat digunakan untuk tipe pengindeks, kita dapat membuatnya setara (seperti yang saya tahu saat ini tidak):

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

Jadi, kita bisa menulis saja

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

Ada beberapa hal yang perlu dipertimbangkan:

  • Bagaimana cara menunjukkan bahwa P hanya dapat berupa string literal?

    • karena string secara teknis memperluas semua literal string, P extends string tidak akan menjadi ideomatis

    • diskusi lain terjadi tentang mengizinkan batasan P super string (# 7265, # 6613, https://github.com/Microsoft/TypeScript/issues/6613#issuecomment-175314703)

    • apakah kita benar-benar perlu peduli tentang ini? kita dapat menggunakan apa pun yang dapat diterima untuk jenis pengindeks - jika T memiliki pengindeks string s atau number s. maka P dapat menjadi string atau number .

  • saat ini, jika kita mengirimkan "something" sebagai argumen kedua, itu akan menjadi tipe string

    • string literals # 10195 bisa menjadi jawabannya

    • atau TS dapat menyimpulkan bahwa P yang lebih spesifik harus digunakan jika dapat diterima

  • pengindeks secara implisit opsional { [i: string]: number /* | undefined */ }

    • bagaimana cara menerapkannya jika kita benar-benar tidak ingin memiliki undefined di domain?

  • tipe inferensi otomatis untuk semua relasi antara P , T dan R adalah kunci untuk membuatnya berfungsi

@weswigham @mhegazy , dan saya telah membahas ini baru-baru ini; kami akan memberi tahu Anda perkembangan apa pun yang kami hadapi, dan perlu diingat bahwa ini hanya membuat prototipe ide.

Ide terkini:

  • Operator keysof Foo untuk mengambil gabungan nama properti dari Foo sebagai tipe literal string.
  • Tipe Foo[K] yang menentukan bahwa untuk beberapa tipe K yang merupakan tipe literal string atau gabungan tipe literal string.

Dari blok dasar ini, jika Anda perlu menyimpulkan literal string sebagai jenis yang sesuai, Anda dapat menulis

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

Di sini, K akan menjadi subtipe dari keysof T yang berarti bahwa itu adalah tipe literal string atau gabungan tipe literal string. Apa pun yang Anda berikan untuk parameter key harus diketik secara kontekstual oleh literal / gabungan literal tersebut, dan disimpulkan sebagai tipe literal string tunggal.

Misalnya

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");

Masalah terbesar adalah bahwa keysof sering kali perlu "menunda" operasinya. Terlalu bersemangat dalam cara mengevaluasi tipe, yang merupakan masalah untuk parameter tipe seperti pada contoh pertama yang saya posting (yaitu kasus yang benar-benar ingin kita selesaikan sebenarnya adalah bagian yang sulit: smile :).

Semoga itu memberi Anda semua pembaruan.

@DanielRosenwasser Terima kasih atas pembaruannya. Saya baru saja melihat @weswigham mengirimkan PR tentang operator keysof , jadi mungkin lebih baik menyerahkan masalah ini kepada kalian.

Saya hanya ingin tahu mengapa Anda memutuskan untuk menyimpang dari sintaks asli yang diusulkan?

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

dan perkenalkan keysof ?

T[prop] kurang umum, dan membutuhkan banyak mesin yang saling terkait. Satu pertanyaan besar di sini adalah bagaimana Anda bahkan menghubungkan isi literal prop dengan nama properti T . Saya bahkan tidak sepenuhnya yakin apa yang akan Anda lakukan. Apakah Anda akan menambahkan parameter tipe implisit? Apakah Anda perlu mengubah perilaku pengetikan kontekstual? Apakah Anda perlu menambahkan sesuatu yang khusus untuk tanda tangan?

Jawabannya mungkin ya untuk semua hal itu. Saya membuat kami menjauh dari itu karena firasat saya memberi tahu saya bahwa lebih baik menggunakan dua konsep yang lebih sederhana dan terpisah dan membangun dari sana. Sisi negatifnya adalah ada sedikit lebih banyak boilerplate dalam kasus-kasus tertentu.

Jika ada pustaka baru yang menggunakan pola semacam ini dan boilerplate itu mempersulit mereka untuk menulis dalam TypeScript, mungkin kita harus mempertimbangkannya. Tetapi secara keseluruhan fitur ini terutama ditujukan untuk melayani konsumen perpustakaan, karena situs penggunaan adalah tempat Anda mendapatkan keuntungan di sini.

@DanielRosenwasser Setelah nyaris tidak masuk ke lubang kelinci. Saya masih tidak dapat menemukan masalah dengan menerapkan ide @SaschaNaz ? Saya pikir keysof berlebihan dalam kasus ini. T[p] sudah berhubungan bahwa p harus menjadi salah satu properti literal dari T .

Pikiran implementasi kasar saya adalah memperkenalkan tipe baru yang disebut PropertyReferencedType .

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

Saat memasukkan fungsi yang dideklarasikan dengan tipe kembalian yaitu PropertyReferencedType atau memasukkan fungsi yang mereferensikan PropertyReferencedType : Tipe dari ElementAccessExpression akan ditambah dengan properti yang mereferensikan simbol dari properti yang diakses.

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

Jadi tipe dengan simbol properti yang direferensikan dapat dialihkan ke PropertyReferencedType . Selama pemeriksaan, referencedProperty harus sesuai dengan p dalam T[p] . Juga tipe induk dari ekspresi akses elemen harus dapat ditetapkan ke T . Dan untuk mempermudah p juga harus menjadi const.

Tipe baru PropertyReferencedType hanya ada di dalam fungsi sebagai "tipe yang tidak terselesaikan". Di situs panggilan seseorang harus menyelesaikan jenisnya dengan p :

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

A PropertyReferencedType hanya menyebar melalui penetapan fungsi dan tidak dapat menyebar melalui ekspresi panggilan, karena PropertyReferencedType hanyalah tipe sementara yang dimaksudkan untuk membantu memeriksa isi fungsi dengan tipe kembalian T[p] .

Jika Anda memperkenalkan operator tipe keysof dan T[K] , apakah itu berarti kita dapat menggunakannya seperti ini:

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 tidak akankah contoh Anda memiliki arti yang sama dengan saya

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 {
    // ...
}

Saya tidak melihat bagaimana tanda tangan akan ditulis untuk Underscore _.pick :

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

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

@rtm Saya menyarankannya di https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -234724380. Meskipun mungkin lebih baik membuka edisi baru, meskipun terkait dengan yang satu ini.

Implementasi sekarang tersedia di # 11929.

Apakah halaman ini membantu?
0 / 5 - 0 peringkat