Typescript: Sugerencia: Tipo Tipo de propiedad

Creado en 28 nov. 2014  ·  76Comentarios  ·  Fuente: microsoft/TypeScript

Motivaciones

Muchas bibliotecas / marcos / patrones de JavaScript involucran cálculos basados ​​en el nombre de propiedad de un objeto. Por ejemplo, el modelo Backbone , la transformación funcional pluck , ImmutableJS se basan todos en dicho mecanismo.

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

Podemos entender fácilmente en esos ejemplos la relación entre la API y la restricción de tipo subyacente.
En el caso del modelo de backbone, es solo una especie de _proxy_ para un objeto de tipo:

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

Para el caso de pluck , es una transformación

T[] => U[]

donde U es el tipo de propiedad de T prop .

Sin embargo, no tenemos forma de expresar tal relación en TypeScript y termina con un tipo dinámico.

Solución propuesta

La solución propuesta es introducir una nueva sintaxis para el tipo T[prop] donde prop es un argumento de la función que usa dicho tipo como valor de retorno o parámetro de tipo.
Con esta nueva sintaxis de tipo podríamos escribir la siguiente definición:

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

De esta manera, cuando usamos nuestro modelo Backbone, TypeScript podría verificar correctamente la llamada get y set .

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

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

La constante prop

Restricción

Obviamente, la constante debe ser de un tipo que pueda usarse como tipo de índice ( string , number , Symbol ).

Caso de indexable

Echemos un vistazo a nuestra definición de Map :

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

Si T es indexable, nuestro mapa hereda este comportamiento:

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

Ahora get tiene por tipo get(prop: string): number .

Interrogatorio

Ahora bien, hay algunos casos en los que me duele pensar en un comportamiento _correcto_, comencemos de nuevo con nuestra definición Map .
Si en lugar de pasar { [index: string]: number } como parámetro de tipo, habríamos dado
{ [index: number]: number } Debería el compilador generar un error?

si usamos pluck con una expresión dinámica para prop en lugar de una constante:

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

o con una constante que no sea una propiedad del tipo pasado como parámetro.
si la llamada a pluck genera un error ya que el compilador no puede inferir el tipo T[prop] , debería T[prop] resolverse en {} o any , si es así, ¿el compilador con --noImplicitAny generará un error?

Fixed Suggestion help wanted

Comentario más útil

@weswigham @mhegazy , y he estado discutiendo esto recientemente; le haremos saber cualquier desarrollo que encontremos, y tenga en cuenta que esto solo es el prototipo de la idea.

Ideas actuales:

  • Un operador keysof Foo para tomar la unión de nombres de propiedad de Foo como tipos de cadena literal.
  • Un tipo Foo[K] que especifica que para algún tipo K que es un tipo de literal de cadena o unión de tipos de literal de cadena.

A partir de estos bloques básicos, si necesita inferir un literal de cadena como el tipo apropiado, puede escribir

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

Aquí, K será un subtipo de keysof T que significa que es un tipo literal de cadena o una unión de tipos de literal de cadena. Cualquier cosa que pase para el parámetro key debe escribirse contextualmente por ese literal / unión de literales, e inferirse como un tipo literal de cadena singleton.

Por ejemplo

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

El mayor problema es que keysof menudo necesita "retrasar" su funcionamiento. Está demasiado ansioso en cómo evalúa un tipo, lo cual es un problema para los parámetros de tipo como en el primer ejemplo que publiqué (es decir, el caso que realmente queremos resolver es en realidad la parte difícil: sonríe :).

Espero que les dé a todos una actualización.

Todos 76 comentarios

Posible duplicado de # 394

Consulte también https://github.com/Microsoft/TypeScript/issues/1003#issuecomment -61171048

@NoelAbrahams Realmente no creo que sea un duplicado del # 394, por el contrario, ambas características son bastante complementarias, algo como:

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

Sería ideal

@fdecampredon

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

¿Qué hacer en este caso?

Es más o menos el mismo caso que el del último párrafo de mi número. Como dije, tenemos opción múltiple, podemos informar un error o inferir any por T[prop] , creo que la segunda solución es más lógica

Gran propuesta. De acuerdo, sería una característica útil.

@fdecampredon , creo que esto es un duplicado. Vea el comentario de Dan y la respuesta correspondiente que contiene la sugerencia de membertypeof .

En mi opinión, todo esto es una gran cantidad de sintaxis nueva para un caso de uso bastante estrecho.

@NoelAbrahams no es lo mismo.

  • memberof T devuelve el tipo cuya instancia solo podría ser una cadena con un nombre de propiedad válido de T instancia.
  • T[prop] devuelve el tipo de la propiedad de T nombrada con una cadena que está representada por prop argumento / variable.

Hay un puente a memberof ese tipo de parámetro prop debe ser memberof T .

En realidad, me gustaría tener un sistema más rico para la inferencia de tipos basado en metadatos de tipos. Pero tal operador es un buen comienzo, así como memberof .

Esto es interesante y deseable. TypeScript aún no funciona bien con marcos con muchas cadenas y esto obviamente ayudaría mucho.

TypeScript no funciona bien con marcos con muchas cadenas

Cierto. Todavía no cambia el hecho de que esta es una sugerencia duplicada.

sin embargo, y esto obviamente ayudaría mucho [a escribir marcos con muchas cadenas]

No estoy seguro de eso. Parece bastante fragmentado y algo específico del patrón de objeto proxy descrito anteriormente. Preferiría un enfoque más holístico del problema de la cuerda mágica en la línea del # 1003.

1003 sugiere any como el tipo de retorno de un getter. Esta propuesta agrega además que al agregar una forma de buscar el tipo de valor también, la combinación resultaría en algo como esto:

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

@spion , ¿quisiste decir # 394? Si siguiera leyendo, vería lo siguiente:

Pensé en el tipo de devolución, pero lo dejé fuera para no hacer que la sugerencia general fuera demasiado grande.

Este fue mi pensamiento inicial, pero tiene problemas. ¿Qué sucede si hay varios argumentos del tipo memberof T , a cuál se refiere membertypeof T ?

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

Esto resuelve el problema "a qué argumento me refiero", pero el nombre membertypeof parece incorrecto y no es un fan del operador que apunta al nombre de la propiedad.

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

Creo que esto funciona mejor.

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

Desafortunadamente, no estoy seguro de tener una gran solución, aunque creo que la última sugerencia tiene un potencial decente.

OK @NoelAbrahams había un comentario en el # 394 que intentaba describir más o menos lo mismo que este.
Ahora creo que T[prop] es quizás un poco más elegante que las diferentes proposiciones de este comentario, y que la proposición en este número va quizás un poco más allá en la reflexión.
Por esta razón, no creo que deba cerrarse como duplicado.
Pero supongo que soy parcial ya que soy yo quien escribió el número;).

@fdecampredon , más, mejor: smiley:

@NoelAbrahams oops, me perdí esa parte. Claro, esos son bastante equivalentes (este no parece introducir otro parámetro genérico, que puede o no ser un problema)

Habiendo echado un vistazo a Flow, creo que sería más elegante con un sistema de tipos un poco más fuerte y tipos especiales en lugar de un estrechamiento de tipos ad hoc.

Lo que queremos decir con get(prop: string): Contact[prop] por ejemplo, es solo una serie de posibles sobrecargas:

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

// is morally equivalent to 

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

Suponiendo la existencia del operador de tipo & (tipos de intersección), este tipo es equivalente a

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

Ahora que hemos traducido nuestro caso no genérico solo en expresiones de tipos sin tratamiento especial (sin [prop] ), podemos abordar la cuestión de los parámetros.

La idea es generar de alguna manera este tipo a partir de un parámetro de tipo. Podríamos definir algunos tipos genéricos ficticios especiales $MapProperties , $Name y $Value para expresar nuestro tipo generado en términos de expresión de tipo único (sin sintaxis especial) sin dejar de insinuar el verificador de tipo que algo debería hacerse.

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

Puede parecer complicado y cercano a las plantillas o tipos dependientes del hombre pobre, pero no se puede evitar cuando alguien quiere que los tipos dependan de los valores.

Otra área donde esto sería útil es iterar sobre las propiedades de un objeto escrito:

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

Estos tipos deberían ser al menos teóricamente posibles de inferir (hay suficiente información de tipos para inferir el resultado del bucle for ), pero probablemente también sea bastante exagerado.

Tenga en cuenta que la próxima versión de react se beneficiaría enormemente de ese enfoque, consulte https://github.com/facebook/react/issues/3398

Como https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -64944856, cuando la cadena es suministrada por una expresión que no sea un literal de cadena, esta función se rompe rápidamente debido al problema de detención. Sin embargo, ¿se puede implementar una versión básica de esto utilizando el problema de aprendizaje del símbolo ES6 (# 2012)?

Aprobado.

Querremos probar esto en una rama experimental para tener una idea de la sintaxis.

¿Me pregunto qué versión de la propuesta se implementará? Es decir, ¿ T[prop] será evaluado en el sitio de la llamada y reemplazado con entusiasmo por un tipo concreto o se convertirá en una nueva forma de variable de tipo?

Creo que deberíamos confiar en una sintaxis más general y menos detallada como se define en # 3779.

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

¿O no es posible inferir el tipo de A?

Solo quiero decir que hice una pequeña herramienta de codegen para facilitar la integración de TS con ImmutableJS mientras esperamos la solución normal: https://www.npmjs.com/package/tsimmutable. Es bastante simple, pero creo que funcionará para la mayoría de los casos de uso. Quizás ayude a alguien.

También quiero señalar que la solución con un tipo de miembro puede no funcionar con 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 Algo como esto podría funcionar:

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>

Esto no excluirá los nodos DOM o los objetos Date, pero no debería permitirse su uso en estructuras inmutables. https://github.com/facebook/immutable-js/wiki/Converting-from-JS-objects

Trabajando gradualmente desde la mejor solución alternativa

Creo que es útil comenzar primero con la mejor solución alternativa disponible actualmente y luego ir trabajando gradualmente a partir de ahí.

Digamos que necesitamos una función que devuelva el valor de una propiedad para cualquier objeto conteniendo no primitivo :

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

Ahora queremos que el valor de retorno tenga el tipo de la propiedad de destino, por lo que se podría agregar otro parámetro de tipo genérico para su tipo:

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

Entonces, un caso de uso con una clase se vería así:

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

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

Y result recibiría correctamente el tipo number .

Sin embargo, parece haber una referencia duplicada a 'miembro' en la llamada: uno está en el parámetro de tipo y el otro es una cadena _literal_ que siempre recibirá la representación de cadena del nombre de la propiedad de destino. La idea de esta propuesta es que estos dos se puedan unificar en un solo parámetro de tipo que solo recibiría la representación de cadena.

Por lo tanto, debe observarse aquí que la cadena también actuaría como un _ parámetro genérico_, y deberá pasarse como un literal para que esto funcione (otros casos podrían ignorar silenciosamente su valor a menos que haya alguna forma de reflexión en tiempo de ejecución disponible). Entonces, para aclarar la semántica, debe haber alguna forma de significar una relación entre el parámetro genérico y la cadena:

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

Y ahora las llamadas se verían así:

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

Que resolvería internamente a:

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

Sin embargo, dado que C contiene una instancia y una propiedad estática denominada member , la expresión genérica de tipo superior T[PName] es ambigua. Dado que la intención aquí es principalmente aplicarlo a las propiedades de las instancias, una solución como el operador typeon podría usarse internamente, lo que también puede mejorar la semántica, ya que algunos interpretan T[PName] como una representación una _referencia de valor_, no necesariamente un tipo:

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

(Esto también funcionaría si T es un tipo de interfaz compatible, ya que typeon admite interfaces)

Para obtener la propiedad estática, la función se llamaría con el propio constructor y el tipo de constructor debería pasarse como typeof C :

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

(Internamente typeon (typeof C) resuelve en typeof C por lo que esto funcionaría)
result ahora recibirá correctamente el tipo string .

Para admitir tipos adicionales de identificadores de propiedad, esto se puede reescribir como:

type PropertyIdentifier = string|number|Symbol;

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

Entonces, hay varias características diferentes que se necesitan para respaldar esto:

  • Permitir que los valores literales se pasen como parámetros genéricos.
  • Permita que esos literales reciban el valor de los argumentos de la función.
  • Permitir una forma de genéricos de tipo superior.
  • Permitir hacer referencia a un tipo de propiedad a través de un parámetro genérico literal en un tipo _generic_ a través de genéricos de tipo superior.

Repensar el problema

Ahora volvamos a la solución original:

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

Hay una ventaja de esta solución alternativa, es que se puede ampliar fácilmente para admitir nombres de ruta más complejos, haciendo referencia no solo a las propiedades directas, sino también a las propiedades de los objetos anidados:

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

    return <P> resultValue;
}

y ahora:

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

result obtendría correctamente el tipo string[] aquí.

La pregunta es, ¿deberían admitirse también las rutas anidadas? ¿Será la función realmente tan útil si no está disponible la función de autocompletar al escribirlos?

Esto sugiere que podría ser posible una solución diferente, que funcionaría en la otra dirección. De vuelta a la solución original:

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

Mirando esto desde la otra dirección, es posible tomar la referencia de tipo en sí y convertirla en una cadena:

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

@typeReferencePathOf es similar en concepto a nameOf pero se aplica a parámetros genéricos. Tomaría la referencia de tipo recibida typeof instance.member (o alternativamente typeon C.member ), extraería la ruta de propiedad member y la convertiría a la cadena literal "member" en la compilación hora. Un @typeReferenceOf complementario menos especializado se resolvería en la cadena completa "typeof instance.member" .

Y ahora:

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

Resolvería:

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

Y una implementación similar para getPath :

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

Resolvería:

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

Dado que @typeReferencePathOf(P) solo se establece como valor predeterminado. El argumento se puede establecer manualmente si es necesario:

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

Si no se proporciona el segundo parámetro de tipo, @typeReferencePathOf() podría resolverse en undefined o, alternativamente, la cadena vacía "" . Si se proporciona un tipo pero no tiene una ruta interna, se resolverá en "" .

Ventajas de este enfoque:

  • No requiere genéricos de tipo superior.
  • No requiere permitir literales como parámetros genéricos.
  • No requiere extender los genéricos de ninguna manera.
  • Permite la finalización automática al escribir la expresión typeof y utiliza un mecanismo existente de verificación de tipos para validarla en lugar de tener que convertirla de una representación de cadena en tiempo de compilación.
  • También se puede utilizar dentro de clases genéricas.
  • No requiere typeon (pero podría admitirlo).
  • Puede aplicarse en otros escenarios.

+1

¡Agradable! Ya comencé a escribir una gran propuesta para abordar el mismo problema, pero encontré esta discusión, así que hablemos aquí.

Aquí hay un extracto de mi problema no enviado:

Pregunta : ¿Cómo se debe declarar la siguiente función en .d.ts para que se pueda usar en contextos donde se supone que debe usarse?

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

Esta función (con ligeras variaciones) se puede encontrar en casi todas las bibliotecas genéricas de "utilidades".

Hay dos casos de uso distintos de mapValues en la naturaleza:

a. _Object-as-a-_ _Dictionary _ donde los nombres de propiedad son dinámicos, los valores tienen el mismo tipo y la función es en general monomórfica (consciente de este tipo)

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

segundo. _Object-as-a-_ _Record _ donde cada propiedad tiene su propio tipo, mientras que la función es paramétricamente polimórfica (por implementación)

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"

La mejor instrumentación disponible para mapValues con TypeScript actual es:

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

No es difícil ver que esta firma coincide perfectamente con el caso de uso _Object-as-a-_ _Dictionary _, mientras que produce un resultado 1 algo sorprendente de la inferencia de tipo cuando se aplica en el caso donde _Object-as-a-_ _Record _ era la intención.

El tipo {p1: T1, p2: T2, ...pn: Tn} será forzado a {[key: string]: T1 | T2 | ... | Tn } combinando todos los tipos y descartando efectivamente toda la información sobre la estructura de un registro:

  • la correcta y esperada 2 a estar bien console.log(res.a[0].toFixed(2)) serán rechazadas por el compilador;
  • el console.log((<number>res['a'][0]).toFixed(2)) aceptado tiene dos lugares incontrolados y propensos a errores: el nombre de propiedad de conversión tipográfica y arbitrario.

1, 2 - para los programadores que migran de ES a TS


Posibles pasos para abordar este problema (y resolver otros también)

1. Introduzca registros variadic con ayuda de tipos de literal de cadena

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. Permitir que los tipos formales se suscriban con otros tipos formales que están restringidos a extender 'cadena'

ejemplo:


declare function mapValues<p extends string, T1[p], T2[p]>(
     obj:{...p: T1[p]}, fn:(arg: T1[p]) => T2[p]
): {...p: T2[p]};
3. Permitir la desestructuración de tipos formales

ejemplo:

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

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

con los tres pasos juntos podríamos escribir

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

Sé que la sintaxis es más detallada que la propuesta por @fdecampredon
pero no puedo imaginar cómo expresar el tipo de mapValues o combineReducers con la propuesta original.

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

con mi propuesta su declaración se verá así:

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

¿Es expresable con la propuesta original?

@spion , @fdecampredon ?

@Artazor Definitivamente una mejora. Ahora parece ser lo suficientemente bueno para expresar mi caso de uso de referencia de esta función:

Suponiendo que user.id y user.name son tipos de columnas de base de datos que contienen number y string consecuencia, es decir, Column<number> y Column<string>

Escribe el tipo de función select que toma:

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

y devuelve Query<{id: number; name: string}>

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

Sin embargo, no estoy seguro de cómo se comprobará e inferirá eso. Parece que puede haber un problema, ya que el tipo T no existe. ¿Debería expresarse el tipo de retorno en una forma desestructurada? es decir

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

@Artazor olvidó preguntar, ¿es realmente necesario el parámetro p extends string ?

¿No sería suficiente algo como esto?

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

es decir, para todas las variables de tipo T [p] en {...p:Column<T[p]>}

editar: otra pregunta, ¿cómo se va a verificar que esto sea correcto?

@spion Tengo tu punto de vista, me has entendido mal (pero es mi culpa), por T[p] he querido decir algo completamente diferente, y verás que p extends string es una cosa crucial aquí. Permítanme explicar mis pensamientos en profundidad.

Consideremos cuatro casos de uso para estructuras de datos: List , Tuple , Dictionary y Record . Nos referiremos a ellos como _estructuras de datos conceptuales_ y los describiremos con las siguientes propiedades:

Estructuras de datos conceptualesAcceso por
a. posición
(número)
segundo. llave
(cuerda)
Cuenta de
artículos
1. variable
(el mismo rol para cada artículo)
ListaDiccionario
2. fijo
(cada elemento juega su propio papel único)
TuplaGrabar

En JavaScript, estos cuatro casos están respaldados por dos formas de almacenamiento: un Array - para a1 y a2 , y un Object - para b1 y b2 .

_Nota 1 _. Para ser honesto, no es correcto decir que Tuple está respaldado por Array , ya que el único tipo de tupla "verdadero" es el tipo del objeto arguments que no es un Array . Sin embargo, son más o menos intercambiables, especialmente en el contexto de la desestructuración y el Function.prototype.apply que abre la puerta a la metaprogramación. TypeScript también aprovecha las matrices para modelar tuplas.

_Nota 2 _. Si bien el concepto completo del Diccionario con claves arbitrarias se introdujo décadas más tarde en forma de ES6 Map , la decisión inicial de Brendan Eich de combinar un Record y un Dictionary limitado (con solo claves de cadena) bajo el mismo capó de Object (donde obj.prop es equivalente a obj["prop"] ) se convirtió en el elemento más controvertido de todo el lenguaje (en mi opinión).
Es tanto la maldición como la bendición de la semántica de JavaScript. Hace que la reflexión sea trivial y anima a los programadores a cambiar libremente entre los niveles de _programación_ y _metaprogramación_ con un coste mental casi nulo (¡incluso sin darse cuenta!). Creo que es la parte esencial del éxito de JavaScript como lenguaje de programación.

Ahora es el momento de que TypeScript proporcione la forma de expresar tipos para esta extraña semántica. Cuando pensamos en el nivel de _programación_ todo está bien:

Tipos a nivel de programaciónAcceso por
a. posición
(número)
segundo. llave
(cuerda)
Cuenta de
artículos
1. variable
(el mismo rol para cada artículo)
T []{[clave: cadena]: T}
2. fijo
(cada elemento juega su propio papel único)
[T1, T2, ...]{clave1: T1, clave2: T2, ...}

Sin embargo, cuando cambiamos al nivel de _meta-programación_, ¡las cosas que estaban arregladas en el nivel de _programación_ de repente se vuelven variables! Y aquí reconocemos repentinamente las relaciones entre tuplas y firmas de funciones y proponemos cosas como # 5453 (Variadic Kinds) que de hecho cubre solo una pequeña (pero bastante importante) parte de las necesidades de metaprogramación: la concatenación de firmas al dar la capacidad de desestructurar una estructura formal. parámetro en tipos de rest-args:

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

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

En caso de que, si también se implementará # 6018, la clase Function podría verse como

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

Es genial pero de todos modos incompleto.

Por ejemplo, imagina que queremos asignar un tipo correcto a la siguiente función:

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

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

Con los tipos variados propuestos no podemos transformar los tipos de la firma. Aquí necesitamos algo más poderoso. De hecho, es deseable tener funciones en tiempo de compilación que puedan operar sobre tipos (como en el Flow de Facebook). Sin embargo, estoy seguro de que esto será posible solo cuando (y si) el sistema de tipos de TypeScript sea lo suficientemente estable como para exponerlo a los programadores finales sin un riesgo significativo de romper la tierra del usuario en una próxima actualización menor. Por lo tanto, necesitamos algo menos radical que el soporte completo de metaprogramación, pero aún así capaz de lidiar con los problemas demostrados.

Para abordar este problema, quiero presentar la noción de _firma de objeto_. Aproximadamente es

  1. el conjunto de nombres de propiedad del objeto, o
  2. el conjunto de claves del diccionario, o
  3. el conjunto de índices numéricos de una matriz
  4. el conjunto de posiciones numéricas de una tupla.

Idealmente, sería bueno tener tipos literales enteros, así como tipos literales de cadena para representar firmas de las matrices o tuplas como

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

las reglas para los literales enteros son congruentes con los literales de cadena;

Luego podemos introducir la sintaxis de abstracción de firmas, que coincide exactamente con la sintaxis de resto / extensión del objeto:

{...Props: T }

con una excepción significativa: aquí Props es el identificador que está ligado al cuantificador 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[]

por lo tanto, se introduce en el ámbito léxico de tipo y se puede utilizar en cualquier lugar de este ámbito como nombre del tipo. Sin embargo, su uso es dual: cuando se usa en lugar del nombre de propiedad rest / spread, representa una abstracción sobre la firma del objeto, cuando se usa un tipo independiente, representa un subtipo de la cadena (o número respectivamente).

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

Y aquí está la parte más sofisticada: _Tipos dependientes de clave_

introducimos una construcción de tipo especial: T for Prop (me gustaría usar esta sintaxis en lugar de T [Prop] que lo confundió) donde Prop es el nombre de la variable de tipo que contiene la abstracción de firma del objeto. Por ejemplo, <Prop extends string, T for Prop> introduce dos tipos formales en el ámbito léxico de tipos, el Prop y el T donde se sabe que para cada valor particular p de Prop habrá su propio tipo T .

¡No decimos que en algún lugar hay un objeto que tiene propiedades Props y sus tipos son T ! Solo introducimos una dependencia funcional entre dos tipos. El tipo T está correlacionado con los miembros del tipo Props, ¡y eso es todo!

Nos da la posibilidad de escribir cosas como

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

sin embargo, el segundo parámetro no se trata como un objeto, sino como el mapa abstracto de identificadores a tipos. En esta perspectiva, T for P se puede usar para representar secuencias abstractas de tipos para tuplas cuando P es un subtipo de número ( @JsonFreeman ?)

Cuando se usa T algún lugar dentro de {...P: .... T .... } , representa exactamente un tipo particular de ese mapa.

Es mi idea principal.
Esperando ansiosamente cualquier pregunta, pensamiento, crítica -)

Correcto, entonces extends string es tener en cuenta matrices (y argumentos variadic) en un caso, y constantes de cadena (como tipos) en el otro. ¡Dulce!

No decimos que en algún lugar hay un objeto que tiene propiedades Props y sus tipos son T! Solo introducimos una dependencia funcional entre dos tipos. El tipo T está correlacionado con los miembros del tipo Props, ¡y eso es todo!

No quise decir eso, lo dije en serio ya que sus tipos son T [p], un diccionario de tipos indexados por p. Si es una buena intuición, me quedaría con eso.

Sin embargo, en general, la sintaxis podría necesitar un poco más de trabajo, pero la idea general se ve increíble.

¿Es posible escribir unwrap para argumentos variados?

editar: no importa, me acabo de dar cuenta de que su extensión propuesta para tipos variadic aborda esto.

Hola todos,
Estoy desconcertado sobre cómo resolver uno de mis problemas y encontré esta discusión.
El problema es:
Tengo el método RpcManager.call(command:Command):Promise<T> , y el uso sería así:

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

Solución creo que podría ser como:

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

Tiene alguna idea sobre esto?

@ antanas-arvasevicius, el último bloque de código en ese ejemplo debería hacer lo que quieras.

parece que tienes más dudas sobre cómo realizar una tarea específica; utilice Stack Overflow o presente un problema si cree que ha encontrado un error del compilador.

Hola, Ryan, gracias por tu respuesta.
Probé el último bloque de código pero no funciona.

Demostración rápida:

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

Lamento no haber usado Stack Overflow, pero pensé que estaba relacionado con este problema :)
¿Debo abrir un nuevo número sobre este tema?

Un parámetro de tipo genérico no consumido evita que la inferencia funcione; en general, _nunca_ debe tener parámetros de tipo sin usar en declaraciones de tipo, ya que no tienen sentido. Si consume T , todo simplemente funciona:

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

¡Eso es simplemente asombroso! ¡Gracias!
No he pensado que el parámetro de tipo se pueda colocar dentro del tipo genérico y
TS lo extraerá.
El 22 de febrero de 2016 a las 11:56 p.m., "Ryan Cavanaugh" [email protected] escribió:

Un parámetro de tipo genérico no consumido evita que la inferencia funcione; en
en general, _nunca_ debe tener parámetros de tipo no utilizados en el tipo
declaraciones, ya que no tienen sentido. Si consumes T, todo simplemente
trabajos:

comando de interfaz{foo: T} clase MyCommand implementa el comando <{status: string}> {foo: {status: string; }} clase RPC {llamada estática(comando: comando): T {retorno; }}
dejar respuesta = RPC.call (new MyCommand ());
console.log (response.status);

-
Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -187404245
.

@ antanas-arvasevicius si está creando API de estilo RPC, tengo algunos documentos que pueden resultarle útiles: https://github.com/alm-tools/alm/blob/master/docs/contributing/ASYNC.md : rose:

Los enfoques anteriores parecen:

  • bastante complicado;
  • no aborde el uso de cadenas en el código. string no son "Buscar todas las referencias" -able, ni refactorable (por ejemplo, renombrar).
  • algunos solo admiten propiedades de "primer nivel" y no expresiones más complejas (que son compatibles con algunos marcos).

Aquí hay otra idea, inspirada en los árboles de expresión de C #. Esta es solo una idea aproximada, ¡nada completamente pensado! La sintaxis es terrible. Solo quiero ver si esto inspira a alguien.

Supongamos que tenemos un tipo especial de cadenas para denotar expresiones.
Llamémoslo type Expr<T, U> = string .
Donde T es el tipo de objeto inicial y U es el tipo de resultado.

Supongamos que podríamos crear una instancia de Expr<T,U> usando una lambda que toma un parámetro de tipo T y realiza un acceso de miembro en él.
Por ejemplo: person => person.address.city .
Cuando esto sucede, toda la lambda se compila en una cadena que contiene cualquier acceso al parámetro, en este caso: "address.city" .

En su lugar, puede usar una cadena simple, que se vería como Expr<any, any> .

Tener este tipo especial Expr en el idioma permite cosas como esa:

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

Esta es básicamente una forma limitada de para qué se usan las expresiones en C #.
¿Crees que esto podría perfeccionarse y conducir a algo interesante?

@fdecampredon @RyanCavanaugh

_ ( @ jods4 - Lo siento, no estoy respondiendo a tu sugerencia aquí. Espero que no quede 'enterrada' a través de los comentarios) _

Creo que el nombre de esta función ('Tipo de propiedad') es muy confuso y muy difícil de entender. ¡Fue _mu_difícil incluso imaginar cuál era el concepto descrito aquí y qué significa en primer lugar!

Primero, ¡no todos los tipos tienen propiedades! undefined y null no lo hacen (aunque estas son solo adiciones recientes al sistema de tipos). Primitivas como number , string , boolean rara vez están indexadas por una propiedad (por ejemplo, 2 ["prop"]? Aunque esto parece funcionar, casi siempre es un error)

Sugeriría nombrar este problema Referencia de tipo de propiedad de interfaz a través de valores literales de cadena . El tema aquí no trata sobre la introducción de un nuevo 'tipo', sino una forma muy particular de hacer referencia a uno existente usando una variable de cadena o un parámetro de función cuyo valor _debe ser conocido en el momento de la compilación_.

Hubiera sido muy beneficioso si esto se hubiera descrito y ejemplificado de la manera más simple posible, fuera del contexto del caso de uso particular:

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.

_Editar: Parece que para implementar esto correctamente, el compilador debe saber con certeza que la cadena asignada a la variable no ha cambiado entre el punto en que se inicializó y el punto al que se hizo referencia en la expresión de tipo. ¿No creo que esto sea muy fácil? ¿Se mantendría siempre esta suposición sobre el comportamiento en tiempo de ejecución? _

_Edit 2: ¿Quizás esto solo funcionaría con const cuando se usa con una variable? _

No estoy seguro de si la intención original era permitir solo la referencia de propiedad en el caso muy particular de que una cadena literal se pase a una función o parámetro de método. p.ej

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

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

¿He generalizado esto más allá de lo que pretendía originalmente?

¿Cómo manejaría esto un caso donde el argumento de la función no es un literal de cadena, es decir, no se conoce en tiempo de compilación? p.ej

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

¿Será un error, o tal vez por defecto any ?

(PD: Si esta era realmente la intención, entonces sugeriré cambiar el nombre de este problema a Referencia de tipo de propiedad de interfaz por función literal de cadena o argumentos de método ; por más extenso que haya resultado, esto es mucho más preciso y explicativo que el título actual).

Aquí hay un ejemplo de referencia simple (suficiente para una ilustración) que muestra lo que esta función necesita para habilitar:

Escriba el tipo de esta función, que:

  • toma un objeto que contiene campos de promesa y
  • devuelve el mismo objeto pero con todos los campos resueltos, cada campo tiene el tipo adecuado.
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)
}

Cuando se llama al siguiente objeto:

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

El resultado res debe ser del tipo {a: number; b: string}


Con esta característica (tal y como la desarrolló @Artazor) la firma sería

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

editar: se corrigió el tipo de devolución anterior, faltaba Promise<...> ^^

@espion

Gracias por tratar de proporcionar un ejemplo, pero en ninguna parte aquí he encontrado una especificación razonable y concisa y un conjunto de ejemplos claros y reducidos que se _detengan_ de aplicaciones prácticas y tratan de resaltar la semántica y los diversos casos extremos de cómo funcionaría esto. incluso algo tan básico como:

function func<T extends object>(name: string): T[name] {
 ...
}
  1. Ha habido muy poco esfuerzo para proporcionar un título eficaz y explicativo (como mencioné, 'Tipo Tipo de propiedad' es un nombre confuso y no muy explicativo para esta función, que no se trata realmente de ningún tipo en particular sino de una referencia de tipo). Mi mejor título sugerido fue _Referencia de tipo de propiedad de interfaz por función literal de cadena o argumentos de método_ (suponiendo que no entendí mal el alcance de esto).
  2. No ha habido una mención clara de lo que sucedería si name no se conoce en tiempo de compilación, es nulo o no está definido, por ejemplo, func<MyType>(getRandomString()) , func<MyType>(undefined) .
  3. No ha habido una mención clara de lo que sucedería si T es un tipo primitivo como number o null .
  4. No se proporcionaron detalles claros sobre si esto solo se aplica a las funciones y si T[name] se puede usar en el cuerpo de la función. Y en ese caso, ¿qué sucede si name se reasigna en el cuerpo de la función y, por lo tanto, es posible que su valor ya no se conozca en el momento de la compilación?
  5. Sin especificación real de una sintaxis: por ejemplo, ¿ T["propName"] o T[propName] funcionarán también por sí mismos? sin una referencia a un parámetro o variable, quiero decir, ¡podría ser útil!
  6. No se menciona ni se comenta si T[name] se puede usar en otros parámetros o incluso fuera de los ámbitos de función, por ejemplo
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. No hay una discusión real sobre la solución relativamente simple usando un parámetro genérico adicional y typeof (aunque se mencionó, pero relativamente tarde después de que la función ya había sido aceptada):

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'

En conclusión: no hay una propuesta real aquí, solo un conjunto de demostraciones de casos de uso. Me sorprende que esto haya sido aceptado o incluso recibido un interés positivo por parte del equipo de TypeScript porque de otra manera no creería que esto estaría a la altura de sus estándares. Lo siento si puedo sonar un poco duro aquí (no es típico de mí), pero ninguna de mis críticas es personal ni está dirigida a ningún individuo en particular.

¿Realmente queremos llegar tan lejos en términos de metaprogramación?

JS es un lenguaje dinámico, el código puede manipular objetos de formas arbitrarias.
Hay límites para lo que podemos modelar en el sistema de tipos y es fácil crear funciones que no se pueden modelar en TS.
La pregunta es más bien: ¿hasta dónde es razonable y útil llegar?

El último ejemplo ( awaitObject ) de @spion es al mismo tiempo:

  • _Muy complejo_ desde el punto de vista del lenguaje (aquí estoy de acuerdo con @malibuzios ).
  • _Muy limitado_ en términos de aplicabilidad. Ese ejemplo elige restricciones específicas y no generaliza bien.

Por cierto, @spion , no obtuvo correctamente el tipo de retorno de su ejemplo ... Devuelve un Promise .

Estamos lejos del problema original, que consistía en escribir API que toman una cadena que representa campos, como _.pluck
Esas API _deben ser_ compatibles porque son comunes y no tan complicadas. No necesitamos metamodelos como { ...p: T[p] } para eso.
Hay varios ejemplos en el OP, algunos más de Aurelia en mi comentario en el nombre del problema .
Un enfoque diferente puede cubrir esos casos de uso, consulte mi comentario anterior para una posible idea.

@ jods4 no es para nada estrecho. Se puede utilizar para una serie de cosas que son imposibles de modelar en el sistema de tipos en este momento. Yo diría que es una de las últimas piezas grandes que faltan en el sistema de tipos TS. Hay muchos ejemplos del mundo real anteriores por @Artazor https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -177287714 las cosas más simples anteriores, el ejemplo de "selección" del generador de consultas SQL, etc.

Esto es awaitObject en bluebird. Es un método real. El tipo es actualmente inútil.

Será mejor una solución más general. Funcionará tanto para los casos simples como para los complejos. En la mitad de los casos faltará una solución con poca potencia y fácilmente causará problemas de compatibilidad si es necesario adoptar una más general en el futuro.

Sí, necesita mucho más trabajo, pero también creo que @Artazor hizo un excelente trabajo analizando y explorando todos los aspectos en los que no habíamos pensado antes. No me quedaría si nos hemos desviado del problema original, solo lo entendemos mejor.

Es posible que necesitemos un nombre mejor para él, lo intentaría, pero generalmente no hago bien estas cosas. ¿Qué tal "Objetos genéricos"?

@spion , solo para

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

(te perdiste una Promesa en la firma de salida)
-)

@ jods4 , estoy de acuerdo con @spion
Hay muchos problemas del mundo real donde { ...p: T[p] } es la solución. Para nombrar uno: react + flux / redux

@espion @Artazor
Solo me preocupa que esto se esté volviendo bastante complejo de especificar con precisión.

La motivación detrás de este problema se ha desviado. Originalmente, se trataba de admitir API que aceptan una cadena para denotar un campo de objeto. Ahora se trata principalmente de API que usan objetos como mapas o registros de una manera muy dinámica. Tenga en cuenta que si podemos matar dos pájaros de un tiro, estoy totalmente de acuerdo.

Si volvemos a las API que aceptan el problema de cadenas, el T[p] no es una solución completa en mi opinión (dije por qué antes).

_Sólo para ser correctos_ awaitObject también debe aceptar propiedades que no sean de Promise, al menos Bluebird props hace. Entonces ahora tenemos:

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

Cambié el tipo de retorno a Promise<T> porque espero que esta notación funcione.
Hay otras sobrecargas, por ejemplo, una que acepta una Promesa para tal objeto (su firma es aún más divertida). Eso significa que la notación { ...p } debe considerarse una coincidencia peor que cualquier otro tipo.

Especificar todo esto va a ser un trabajo duro. Yo diría que es el siguiente paso si quieres impulsar esto.

@espion @ jods4

Quería mencionar que esta característica no se trata de genéricos ni de tipos polimórficos de tipo superior. Esta es simplemente una sintaxis para hacer referencia al tipo de una propiedad a través de un tipo contenedor (con algunas extensiones "avanzadas" que se describen a continuación), que no está tan lejos de typeof en concepto. Ejemplo:

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

Ahora compare con:

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

let y: typeof x.abcd;

Hay dos diferencias principales con typeof . A diferencia de typeof , esto se aplica a tipos (al menos no primitivos, un hecho que a menudo se omite aquí) en lugar de a instancias. Además (y esto es algo importante) se extendió para admitir el uso de constantes de cadena literales (que deben conocerse en el momento de la compilación) como descriptores de ruta de propiedad y genéricos:

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

Sin embargo, técnicamente typeof podría haberse extendido para admitir cadenas literales (esto incluye el caso en el que x tiene un tipo genérico):

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

Y con esta extensión también podría usarse para resolver algunos de los casos de destino aquí:

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

typeof sin embargo es más poderoso ya que admite una cantidad indefinida de niveles de anidación:

let y: typeof x.prop.childProp.deeperChildProp

Y este solo va un nivel. Es decir, no está planeado (hasta donde yo sé) apoyar:

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

Creo que el alcance de esta propuesta (si es que a esto se le puede llamar una propuesta en su nivel de vaguedad) es demasiado estrecho. Puede ayudar a resolver un problema en particular (aunque puede haber soluciones alternativas), lo que parece hacer que muchas personas estén ansiosas por promoverlo. Sin embargo, también consume una valiosa sintaxis del lenguaje. Sin un plan más amplio y un enfoque orientado al diseño, no parece prudente introducir apresuradamente algo que a partir de esta fecha ni siquiera tiene una especificación clara.

_Ediciones: se corrigieron algunos errores obvios en los ejemplos de código_

Investigué un poco sobre la alternativa typeof :

Se ha aprobado el soporte de idioma futuro para typeof x["abcd"] y typeof x[42] , y ahora se incluye en el # 6606, que está actualmente en desarrollo (hay una implementación en funcionamiento).

Esto va a la mitad del camino. Con estos en su lugar, el resto se puede hacer en varias etapas:

(1) Agregue soporte para constantes literales de cadena (o incluso constantes numéricas, ¿podría ser útil para tuplas?) Como especificadores de propiedad en expresiones de tipo, por ejemplo:

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

(2) Permitir aplicar estos especificadores a tipos genéricos

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

(3) Permitir que se pasen estos especificadores y, en la práctica, "instanciar" las referencias, a través de argumentos de función ( typeof list[0] está planeado, así que creo que esto podría cubrir casos más complejos como 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 tipo aquí es el propuesto en # 1809)

Aunque es más potente (por ejemplo, puede admitir typeof obj[propName][nestedPropName] ), es posible que este enfoque alternativo no cubra todos los casos descritos aquí. Por favor, avíseme si hay ejemplos que no parecen ser manejados por esto (un escenario que me viene a la mente es cuando la instancia del objeto no se pasa en absoluto, sin embargo, es difícil para mí en este punto imaginar un caso en el que eso sería necesario, aunque supongo que es posible).

_Ediciones: se corrigieron algunos errores en el código_

@malibuzios
Algunos pensamientos:

  • Esto corrige la definición de API pero no ayuda a la persona que llama. Todavía necesitamos algún tipo de nameof o Expression en el sitio de la llamada.
  • Funciona para definiciones .d.ts , pero generalmente no será inferible estáticamente o incluso verificable en funciones / implementaciones de TS.

Cuanto más lo pienso, más parece Expression<T, U> la solución para ese tipo de API. Soluciona los problemas del sitio de llamadas, la mecanografía del resultado y puede ser inferible + verificable en una implementación.

@ jods4

Mencionaste en un comentario anterior muy arriba que pluck podría implementarse con expresiones. Aunque solía estar muy familiarizado con C #, nunca experimenté realmente con ellos, así que admito que realmente no los entiendo muy bien en este momento.

De todos modos, solo quería mencionar que TypeScript admite _type argument inference_ , por lo que en lugar de usar pluck , uno puede usar map y lograr el mismo resultado, que ni siquiera requeriría especificar un parámetro genérico ( se infiere) y también proporcionaría una verificación y finalización completa del tipo:

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

Donde map (muy simplificado para demostración) se declara como

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

Este mismo patrón de inferencia se puede usar como un 'truco' para pasar cualquier resultado de un lambda arbitrario (que ni siquiera necesita ser llamado) a una función y establecer su tipo de retorno:

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' 

_Editar: ¡podría parecer una tontería pasarlo en una lambda aquí y no solo la cosa en sí! sin embargo, en casos más complejos como los objetos anidados (por ejemplo, x.prop.Childprop ) puede haber referencias nulas o indefinidas que pueden generar errores. Tenerlo en una lambda, que no necesariamente se llama, evita eso.

Admito que no estoy muy familiarizado con algunos de los casos de uso discutidos aquí. Personalmente, nunca sentí la necesidad de llamar a una función que toma un nombre de propiedad como cadena. Pasar una lambda (donde las ventajas que describe también son válidas) en combinación con la interfaz de argumento de tipo suele ser suficiente para resolver muchos problemas comunes.

La razón por la que sugerí el enfoque alternativo typeof fue principalmente porque parece cubrir los mismos casos de uso y proporcionar la misma funcionalidad para lo que la gente describe aquí. ¿Lo usaría yo mismo? No lo sé, nunca sentí una necesidad real de hacerlo (podría ser simplemente que casi nunca uso bibliotecas externas como subrayado, casi siempre desarrollo funciones de utilidad por mí mismo, según sea necesario).

@malibuzios Es cierto, pluck es un poco tonto con lambdas + map ahora incorporados.

En este comentario , doy 5 API diferentes que toman cadenas: todas están conectadas para observar cambios.

@ jods4 y otros

Solo quería mencionar que cuando el # 6606 esté completo, implementando solo la etapa 1 (permitir que los literales de cadena constantes se usen como especificadores de propiedad), algunas de las funciones necesarias aquí se pueden lograr, aunque no tan elegantemente como podría (puede requerir el nombre de propiedad que se declarará como una constante, agregando una declaración adicional).

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

Sin embargo, la cantidad de esfuerzo para implementar esto puede ser significativamente menor que la implementación de las etapas 2 y 3 también (no estoy seguro, pero es posible que la 1 ya esté cubierta por # 6606). Con la etapa 2 implementada, esto también cubriría el caso en el que x tiene un tipo genérico (pero no estoy seguro de si eso es realmente necesario).

Editar: la razón por la que usé una constante externa y no solo escribí el nombre de la propiedad dos veces fue no solo para reducir la escritura, sino también para garantizar que los nombres siempre coincidan, aunque esto aún no se puede usar con herramientas como "renombrar" y "buscar todo referencias ", lo que me parece una seria desventaja.

@ jods4 Sigo buscando una solución mejor que no utilice cadenas. Intentaré pensar qué se puede hacer con nameof y sus variantes.

Aquí tienes otra idea.

Dado que los literales de cadena ya se admiten como tipos, por ejemplo, uno ya puede escribir:

let x: "HELLO"; 

Se puede ver una cadena literal pasada a una función como una forma de especialización genérica.

(_Editar: estos ejemplos se corrigieron para garantizar que s sea ​​inmutable en el cuerpo de la función, aunque const no se admite en las posiciones de los parámetros en este punto (no estoy seguro de readonly aunque)._) :

function func(const s: string)

El tipo de parámetro string asociado con s puede verse aquí como un genérico implícito (ya que puede estar especializado en cadenas literales). Para mayor claridad, lo escribiré de manera más explícita (aunque no estoy seguro de si realmente es necesario):

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

podría especializarse internamente en:

function func(s: "TEST")

Y ahora esto se puede usar como una forma alternativa de "pasar" el nombre de la propiedad, que creo que captura mejor la semántica aquí.

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

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

observeProperty(x, nameof(x.age))

Dado que en T[S] , tanto T como S son parámetros genéricos. Parece más natural tener dos tipos combinados en una posición de tipo que mezclar elementos de alcance de tipo y tiempo de ejecución (por ejemplo, T[someString] ).

_Editaciones: ejemplos reescritos para tener un tipo de parámetro de cadena inmutable.

Como estoy usando TS 1.8.7 y no la última versión, no sabía que en versiones más recientes en

const x = "Hello";

El tipo de x ya se infiere como el tipo literal "Hello" , (es decir: x: "Hello" ) lo que tiene mucho sentido (ver # 6554).

Entonces, naturalmente, si hubiera una manera de definir un parámetro como const (¿o tal vez readonly también funcionaría?):

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

Entonces creo que esto podría sostenerse:

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

Dado que parte de esto es bastante nuevo y se basa en características de lenguaje recientes, intentaré resumirlo lo más claramente posible:

(1) Cuando una variable de cadena const (¿y tal vez readonly también?) Recibe un valor que se conoce en tiempo de compilación, la variable recibe automáticamente un tipo literal que tiene el mismo valor ( Creo que este es un comportamiento reciente que no ocurre en 1.8.x ), por ejemplo

const x = "ABCD";

El tipo de x se infiere que es "ABCD" , no string !, Por ejemplo, se puede indicar esto como x: "ABCD" .

(2) Si se permitieran los parámetros de función readonly , un parámetro readonly con un tipo genérico naturalmente especializaría su tipo en un literal de cadena cuando se reciba como argumento, ¡ya que el parámetro es inmutable!

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

Aquí S se ha resuelto en el tipo "ABCD" , ¡no string !

Sin embargo, si el parámetro str no fuera inmutable, el compilador no puede garantizar que no se reasigne en el cuerpo de la función, por lo tanto, el tipo inferido sería solo string .

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

func("ABCD");

(3) Es posible aprovechar esto y modificar la propuesta original para que el especificador de propiedad sea una referencia a un tipo , que debería restringirse para ser una derivada de string (en algunos casos incluso puede ser apropiado restringirlo solo a tipos literales singleton, aunque actualmente no hay realmente una manera de hacerlo), en lugar de una entidad en tiempo de ejecución:

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

Llamar a esto no requeriría especificar explícitamente ningún argumento de tipo, ya que TypeScript admite la inferencia de argumentos de tipo:

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'

_Ediciones: se corrigieron algunos errores ortográficos y de código.
_Nota: una versión generalizada y extendida de este enfoque modificado ahora también se rastrea en # 7730._

Aquí hay otro uso del tipo de propiedad (o "genéricos indexados" como me gusta llamarlos últimamente) que surgió en una discusión con @Raynos

Ya podemos escribir el siguiente verificador general para matrices:

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

Una vez que tenemos genéricos indexados, también podríamos escribir un verificador general para objetos:

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

que junto con los comprobadores primitivos para cadena, número, booleano, nulo e indefinido le permitirían escribir cosas como esta:

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

y obtener el resultado con el verificador de tiempo de ejecución correcto _y_ compilar el tipo de protección de tiempo, al mismo tiempo :)

¿Alguien ha trabajado en esto todavía, o está en el radar de alguien? Esta será una mejora importante en la cantidad de bibliotecas estándar que pueden usar mecanografía, incluidas las de lodash o ramda y muchas interfaces de base de datos.

@malibuzios Creo que te estás acercando a la sugerencia de @Artazor :)

Para abordar sus inquietudes:

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

Esto sería

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

De esta manera, el nombre se bloqueará en el tipo más específico (un tipo de cadena constante) cuando se llame con:

func("test")

El tipo S convierte en "test" (no string ). Como tal, str no se puede reasignar a un valor diferente. Si lo intentaste, por ejemplo

str = "other"

El compilador podría producir un error (a menos que haya problemas de solidez de varianza;))

En lugar de simplemente obtener el tipo de una propiedad, me gustaría tener la opción de obtener un súper tipo de tipo arbitrario.

Entonces, mi sugerencia es agregar la siguiente sintaxis: En lugar de solo tener T[prop] , me gustaría agregar la sintaxis T[...props] . Donde props debe ser una matriz de miembros de T . Y el tipo resultante es un super tipo de T con miembros de T definidos en props .

Creo que sería muy útil en Sequelize, un ORM popular para node.js. Por razones de seguridad y de rendimiento, es aconsejable consultar los atributos en una tabla que necesita usar. Eso a menudo significa súper tipo de un tipo.

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

(En mi ejemplo, incluye el operador memberof , que es lo que espera que sea, y también la expresión options.attributes que es lo mismo que typeof options.attributes , pero creo que typeof operador

Si nadie insiste, comencé a trabajar en esto.

¿Qué piensa sobre la seguridad de tipos dentro de la función, es decir, asegurarse de que una declaración de retorno devuelva algo asignable a un tipo de retorno?

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

Además, el nombre "Tipo de propiedad" utilizado aquí parece un poco incorrecto. En cierto modo, establece que el tipo tiene propiedades, que casi todos los tipos tienen.

¿Qué pasa con el "Tipo de propiedad referenciada"?

¿Qué piensas sobre la seguridad de tipos dentro de la función?

Mi lluvia de ideas:

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
}

Y no permitir cadena normal en la línea de retorno.

El problema de

¿Algún comentario del equipo de TS sobre https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -239653337?

@RyanCavanaugh @mhegazy, etc.

En cuanto al nombre, comencé a llamar a esta función (al menos la forma que propuso @Artazor ) "Genéricos indexados"

Una solución desde otro ángulo de visión podría ser para este problema. No estoy seguro de si ya lo trajeron, es un hilo largo. Desarrollando una sugerencia genérica de cadena, podríamos extender la firma de indexación. Dado que los literales de cadena se pueden usar para el tipo de indexador, podríamos tenerlos como equivalentes (ya que sé que no lo son en este momento):

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

Entonces, podríamos escribir entonces

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

Hay algunas cosas que se deben considerar:

  • ¿Cómo indicar que P solo puede ser una cadena literal?

    • dado que la cadena técnicamente extiende todos los literales de cadena, P extends string no sería muy ideomático

    • se está produciendo otra discusión sobre permitir la restricción P super string (# 7265, # 6613, https://github.com/Microsoft/TypeScript/issues/6613#issuecomment-175314703)

    • ¿Realmente debemos preocuparnos por esto? podemos usar lo que sea aceptable para el tipo de indexador, si T tiene un indexador de string s o number s. entonces P puede ser string o number .

  • actualmente, si pasamos "something" como segundo argumento, será del tipo string

    • los literales de cadena # 10195 podrían ser la respuesta

    • o TS podría inferir que se debe usar un P más específico si es aceptable

  • los indexadores son implícitamente opcionales { [i: string]: number /* | undefined */ }

    • ¿Cómo hacer cumplir si realmente no queremos tener undefined en el dominio?

  • inferencia de tipo automática para todas las relaciones entre P , T y R es una clave para que funcione

@weswigham @mhegazy , y he estado discutiendo esto recientemente; le haremos saber cualquier desarrollo que encontremos, y tenga en cuenta que esto solo es el prototipo de la idea.

Ideas actuales:

  • Un operador keysof Foo para tomar la unión de nombres de propiedad de Foo como tipos de cadena literal.
  • Un tipo Foo[K] que especifica que para algún tipo K que es un tipo de literal de cadena o unión de tipos de literal de cadena.

A partir de estos bloques básicos, si necesita inferir un literal de cadena como el tipo apropiado, puede escribir

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

Aquí, K será un subtipo de keysof T que significa que es un tipo literal de cadena o una unión de tipos de literal de cadena. Cualquier cosa que pase para el parámetro key debe escribirse contextualmente por ese literal / unión de literales, e inferirse como un tipo literal de cadena singleton.

Por ejemplo

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

El mayor problema es que keysof menudo necesita "retrasar" su funcionamiento. Está demasiado ansioso en cómo evalúa un tipo, lo cual es un problema para los parámetros de tipo como en el primer ejemplo que publiqué (es decir, el caso que realmente queremos resolver es en realidad la parte difícil: sonríe :).

Espero que les dé a todos una actualización.

@DanielRosenwasser Gracias por la actualización. Acabo de ver que @weswigham envió un PR sobre el operador keysof , por lo que tal vez sea mejor dejarles este problema a ustedes.

Me pregunto por qué decidió apartarse de la sintaxis propuesta original.

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

e introducir keysof ?

T[prop] es menos general y requiere mucha maquinaria entrelazada. Una gran pregunta aquí es cómo relacionaría el contenido literal de prop con los nombres de propiedad de T . Ni siquiera estoy completamente seguro de lo que harías. ¿Agregaría un parámetro de tipo implícito? ¿Necesitaría cambiar el comportamiento de escritura contextual? ¿Necesitaría agregar algo especial a las firmas?

La respuesta probablemente sea sí a todas esas cosas. Nos alejé de eso porque mi instinto me dijo que era mejor usar dos conceptos separados más simples y construir a partir de ahí. La desventaja es que hay un poco más de repetición en ciertos casos.

Si hay bibliotecas más nuevas que usan este tipo de patrones y ese modelo estándar les dificulta escribir en TypeScript, entonces tal vez deberíamos considerarlo. Pero en general, esta función está destinada principalmente a servir a los consumidores de la biblioteca, porque el sitio de uso es donde obtiene los beneficios aquí de todos modos.

@DanielRosenwasser Habiendo apenas bajado por la madriguera del conejo. ¿Todavía no puedo encontrar ningún problema con la implementación de la idea de @SaschaNaz ? Creo que keysof es redundante en este caso. T[p] ya relata que p debe ser uno de los apoyos literales de T .

Mi idea de implementación aproximada fue introducir un nuevo tipo llamado PropertyReferencedType .

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

Al ingresar una función declarada con un tipo de retorno que es de PropertyReferencedType o ingresar una función que hace referencia a PropertyReferencedType : Un tipo de ElementAccessExpression se aumentará con una propiedad que hace referencia al símbolo de la propiedad a la que se accede.

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

Por lo tanto, un tipo con un símbolo de propiedad referenciado se puede asignar a PropertyReferencedType . Durante la comprobación, el referencedProperty debe corresponder a p en T[p] . Además, el tipo padre de una expresión de acceso a elementos debe poder asignarse a T . Y para facilitar las cosas, p también debe ser constante.

El nuevo tipo PropertyReferencedType solo existe dentro de la función como un "tipo no resuelto". En el sitio de llamada uno tiene que resolver el tipo con p :

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

Un PropertyReferencedType solo se propaga a través de asignaciones de funciones y no se puede propagar a través de expresiones de llamada, porque un PropertyReferencedType es solo un tipo temporal destinado a ayudar a verificar el cuerpo de una función con el tipo de retorno T[p] .

Si introduce los operadores de tipo keysof y T[K] , ¿significaría que podríamos usarlos así?

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 ¿tu ejemplo no tendría el mismo significado con mi

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

No veo cómo se escribiría la firma para el _.pick de Underscore:

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

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

@rtm Lo sugerí en https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -234724380. Aunque podría ser mejor abrir un nuevo número, aunque esté relacionado con este.

Implementación ahora disponible en # 11929.

¿Fue útil esta página
0 / 5 - 0 calificaciones