Typescript: Sugestão: Tipo tipo de propriedade

Criado em 28 nov. 2014  ·  76Comentários  ·  Fonte: microsoft/TypeScript

Motivações

Muitas bibliotecas / estruturas / padrões JavaScript envolvem computação com base no nome da propriedade de um objeto. Por exemplo, modelo de backbone , transformação funcional pluck , ImmutableJS são todos baseados em tal 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 compreender facilmente nesses exemplos a relação entre a API e a restrição de tipo subjacente.
No caso do modelo de backbone, é apenas uma espécie de _proxy_ para um objeto do tipo:

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

Para o caso de pluck , é uma transformação

T[] => U[]

onde U é o tipo de uma propriedade de T prop .

Porém não temos como expressar tal relação no TypeScript, e termina com tipo dinâmico.

Solução proposta

A solução proposta é introduzir uma nova sintaxe para o tipo T[prop] onde prop é um argumento da função usando esse tipo como valor de retorno ou parâmetro de tipo.
Com esta nova sintaxe de tipo, poderíamos escrever a seguinte definição:

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

Dessa forma, quando usamos nosso modelo de Backbone, o TypeScript pode verificar o tipo corretamente a chamada get e set .

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

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

A constante prop

Restrição

Obviamente, a constante deve ser de um tipo que possa ser usado como tipo de índice ( string , number , Symbol ).

Caso indexável

Vamos dar uma olhada em nossa Map definição:

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

Se T é indexável, nosso mapa herda deste comportamento:

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

Agora get tem para o tipo get(prop: string): number .

Interrogatório

Agora, há alguns casos em que tenho dificuldade em pensar em um comportamento _correto_, vamos começar novamente com nossa definição de Map .
Se ao invés de passar { [index: string]: number } como parâmetro de tipo, teríamos dado
{ [index: number]: number } o compilador deve gerar um erro?

se usarmos pluck com uma expressão dinâmica para prop em vez de uma constante:

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

ou com uma constante que não seja uma propriedade do tipo passado como parâmetro.
se a chamada para pluck gerar um erro, pois o compilador não pode inferir o tipo T[prop] , deve ser resolvido T[prop] para {} ou any , em caso afirmativo, o compilador com --noImplicitAny gerar um erro?

Fixed Suggestion help wanted

Comentários muito úteis

@weswigham @mhegazy , e tenho discutido isso recentemente; avisaremos você sobre quaisquer desenvolvimentos que encontrarmos e tenha em mente que isso é apenas um protótipo da ideia.

Ideias atuais:

  • Um operador keysof Foo para obter a união dos nomes das propriedades de Foo como tipos de literal de string.
  • Um tipo Foo[K] que especifica que para algum tipo K que é um tipo de literal de string ou união de tipos de literais de string.

A partir desses blocos básicos, se você precisar inferir um literal de string como o tipo apropriado, você pode escrever

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

Aqui, K será um subtipo de keysof T que significa que é um tipo literal de string ou uma união de tipos literais de string. Qualquer coisa que você passar para o parâmetro key deve ser contextualmente digitada por esse literal / união de literais e inferido como um tipo de literal de string singleton.

Por exemplo

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

O maior problema é que keysof frequentemente precisa "atrasar" sua operação. Ele está muito ansioso para avaliar um tipo, o que é um problema para parâmetros de tipo como no primeiro exemplo que postei (ou seja, o caso que realmente queremos resolver é na verdade a parte difícil: sorria :).

Espero que isso dê a vocês uma atualização.

Todos 76 comentários

Possível duplicata de # 394

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

@NoelAbrahams Eu realmente não acho que seja uma duplicata do # 394, pelo contrário, os dois recursos são bastante complementares, algo como:

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

Seria ideal

@fdecampredon

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

O que fazer neste caso?

É mais ou menos o mesmo caso que o do último parágrafo da minha edição. Como eu disse, temos múltipla escolha, podemos relatar um erro ou inferir any para T[prop] , acho que a segunda solução é mais lógica

Ótima proposta. Concordo, seria um recurso útil.

@fdecampredon , acredito que seja uma duplicata. Veja o comentário de Dan e a resposta correspondente que contém a sugestão para membertypeof .

IMO tudo isso é um monte de nova sintaxe para um caso de uso bastante restrito.

@NoelAbrahams não é a mesma coisa.

  • memberof T retorna o tipo cuja instância só poderia ser uma string com o nome de propriedade válido de T instância.
  • T[prop] retorna o tipo da propriedade de T nomeada com string que é representada por prop argumento / variável.

Há um desvio para memberof que o tipo de parâmetro prop deve ser memberof T .

Na verdade, eu gostaria de ter um sistema mais rico para inferência de tipo com base em metadados de tipo. Mas tal operador é um bom começo, assim como memberof .

Isso é interessante e desejável. O TypeScript ainda não se sai bem com estruturas pesadas de string e isso obviamente ajudaria muito.

TypeScript não funciona bem com estruturas pesadas de string

Verdadeiro. Ainda não muda o fato de que esta é uma sugestão duplicada.

ainda e isso obviamente ajudaria muito [para digitar estruturas pesadas de string]

Não tenho certeza sobre isso. Parece bastante fragmentado e um tanto específico para o padrão de objeto proxy descrito acima. Eu preferiria muito mais uma abordagem holística para o problema da corda mágica ao longo das linhas de # 1003.

1003 sugere any como o tipo de retorno de um getter. Esta proposta acrescenta que, ao adicionar uma maneira de pesquisar o tipo de valor também - a combinação resultaria em algo assim:

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

@spion , você quis dizer # 394? Se você lesse mais adiante, veria o seguinte:

Eu pensei sobre o tipo de retorno, mas deixei de fora para não tornar a sugestão geral muito grande de uma mordida.

Este foi o meu pensamento inicial, mas tem problemas. E se houver vários argumentos do tipo memberof T , a qual membertypeof T refere?

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

Isso resolve o problema "a qual argumento estou me referindo", mas o nome membertypeof parece errado e não é um fã do operador que visa o nome da propriedade.

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

Eu acho que isso funciona melhor.

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

Infelizmente, não tenho certeza se tenho uma ótima solução, embora eu acredite que a última sugestão tenha um potencial decente.

OK @NoelAbrahams, houve um comentário no # 394 que tentava descrever mais ou menos a mesma coisa que este.
Agora eu acho que T[prop] é talvez um pouco mais elegante do que as diferentes proposições deste comentário, e que a proposição desta edição vai talvez um pouco mais adiante na reflexão.
Por este motivo, não creio que deva ser encerrado como duplicado.
Mas acho que sou tendencioso, já que fui eu quem escreveu a edição;

@fdecampredon , mais, melhor: smiley:

@NoelAbrahams ops , eu perdi essa parte. Claro, esses são praticamente equivalentes (este não parece introduzir outro parâmetro genérico, o que pode ou não ser um problema)

Tendo dado uma olhada no Flow, acho que seria mais elegante com um sistema de tipos um pouco mais forte e tipos especiais, em vez de um estreitamento de tipo ad hoc.

O que queremos dizer com get(prop: string): Contact[prop] por exemplo, é apenas uma série de possíveis sobrecargas:

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

// is morally equivalent to 

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

Assumindo a existência do operador do tipo & (tipos de interseção), este é o tipo equivalente a

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

Agora que traduzimos nosso caso não genérico em apenas expressões de tipos sem tratamento especial (sem [prop] ), podemos abordar a questão dos parâmetros.

A ideia é gerar um pouco esse tipo a partir de um parâmetro de tipo. Podemos definir alguns tipos genéricos fictícios especiais $MapProperties , $Name e $Value para expressar nosso tipo gerado em termos de apenas expressão de tipo (sem sintaxe especial), enquanto ainda sugere o verificador de tipo que algo deve ser feito.

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

Pode parecer complicado e próximo de modelos ou tipos dependentes de pobres, mas não pode ser evitado quando alguém deseja que os tipos dependam de valores.

Outra área em que isso seria útil é a iteração das propriedades de um objeto digitado:

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

Esses tipos deveriam ser pelo menos teoricamente possíveis de inferir (há informações de tipo suficientes para inferir o resultado do loop for ), mas também é provavelmente um tanto extenso.

Observe que a próxima versão do react teria um grande benefício com essa abordagem, consulte https://github.com/facebook/react/issues/3398

Como https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -64944856, quando a string é fornecida por uma expressão diferente de um literal de string, esse recurso é rapidamente interrompido devido ao problema de parada. No entanto, uma versão básica disso ainda pode ser implementada usando o aprendizado do problema do símbolo ES6 (# 2012)?

Aprovado.

Queremos tentar isso em um branch experimental para ter uma ideia da sintaxe.

Você está se perguntando qual versão da proposta será implementada? Ou seja, T[prop] será avaliado no local da chamada e prontamente substituído por um tipo concreto ou se tornará uma nova forma de variável de tipo?

Acho que devemos confiar em uma sintaxe mais geral e menos detalhada, conforme definido em # 3779.

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

Ou não é possível inferir o tipo de A?

Só quero dizer que fiz uma pequena ferramenta codegen para facilitar a integração do TS com ImmutableJS enquanto esperamos pela solução normal: https://www.npmjs.com/package/tsimmutable. É muito simples, mas acho que funcionará para a maioria dos casos de uso. Talvez ajude alguém.

Também quero observar que a solução com um tipo de membro pode não funcionar com 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 assim pode 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>

Isso não excluirá nós DOM ou objetos Date, mas não deve ser permitido usá-los em estruturas imutáveis. https://github.com/facebook/immutable-js/wiki/Converting-from-JS-objects

Trabalhando gradualmente com a melhor solução alternativa

Acho que é útil começar com a melhor solução alternativa atualmente disponível e, em seguida, trabalhar gradualmente a partir daí.

Digamos que precisamos de uma função que retorne o valor de uma propriedade para qualquer objeto não primitivo que contém:

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

Agora queremos que o valor de retorno tenha o tipo da propriedade de destino, portanto, outro parâmetro de tipo genérico pode ser adicionado para seu tipo:

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

Portanto, um caso de uso com uma classe seria:

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

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

E result receberia corretamente o tipo number .

No entanto, parece haver uma referência duplicada a 'membro' na chamada: um está no parâmetro de tipo e o outro é uma string _literal_ que sempre receberá a representação da string do nome da propriedade de destino. A ideia dessa proposta é que esses dois possam ser unificados em um único parâmetro de tipo que receberia apenas a representação string.

Portanto, deve ser observado aqui que a string também atuaria como um _parâmetro genérico_ e precisará ser passada como um literal para que isso funcione (outros casos podem ignorar silenciosamente seu valor, a menos que alguma forma de reflexão de tempo de execução esteja disponível). Portanto, para deixar a semântica clara, deve haver alguma maneira de significar uma relação entre o parâmetro genérico e a string:

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

E agora as chamadas seriam assim:

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

Que internamente resolveria:

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

No entanto, como C contém uma instância e uma propriedade estática chamada member , a expressão genérica de tipo superior T[PName] é ambígua. Uma vez que a intenção aqui é principalmente aplicá-lo a propriedades de instâncias, uma solução como o operador typeon proposto poderia ser usado internamente, o que também pode melhorar a semântica, já que T[PName] é interpretado por alguns como representando uma _referência de valor_, não necessariamente um tipo:

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

(Isso também funcionaria se T fosse um tipo de interface compatível, já que typeon oferece suporte a interfaces)

Para obter a propriedade estática, a função seria chamada com o próprio construtor e o tipo de construtor deveria ser passado como typeof C :

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

(Internamente typeon (typeof C) resolve para typeof C então isso funcionaria)
result agora receberá corretamente o tipo string .

Para oferecer suporte a tipos adicionais de identificadores de propriedade, isso pode ser reescrito como:

type PropertyIdentifier = string|number|Symbol;

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

Portanto, há vários recursos diferentes necessários para oferecer suporte a isso:

  • Permite que valores literais sejam passados ​​como parâmetros genéricos.
  • Permita que esses literais recebam o valor dos argumentos da função.
  • Permitir uma forma de genéricos de tipo superior.
  • Permite referenciar um tipo de propriedade por meio de um parâmetro genérico literal em um tipo _generic_ por meio de genéricos de tipo superior.

Repensando o problema

Agora, vamos voltar à solução original:

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

Há uma vantagem sobre essa solução alternativa, é que ela pode ser facilmente estendida para oferecer suporte a nomes de caminho mais complexos, referenciando não apenas propriedades diretas, mas propriedades de objetos aninhados:

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

    return <P> resultValue;
}

e agora:

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

result obteria corretamente o tipo string[] aqui.

A questão é: os caminhos aninhados também devem ser suportados? O recurso será realmente útil se nenhum preenchimento automático estiver disponível ao digitá-los?

Isso sugere que uma solução diferente pode ser possível, o que funcionaria na outra direção. De volta à solução alternativa original:

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

Olhando por outra direção, é possível pegar a própria referência de tipo e convertê-la em uma string:

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

@typeReferencePathOf é semelhante em conceito a nameOf mas se aplica a parâmetros genéricos. Ele pegaria a referência de tipo recebida typeof instance.member (ou alternativamente typeon C.member ), extrairia o caminho da propriedade member e o converteria na string literal "member" na compilação Tempo. Um complementar menos especializado @typeReferenceOf resolveria para a string completa "typeof instance.member" .

Então agora:

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

Resolveria:

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

E uma implementação semelhante para getPath :

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

Resolveria:

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

Uma vez que @typeReferencePathOf(P) é apenas definido como um valor padrão. O argumento pode ser declarado manualmente, se necessário:

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

Se o segundo parâmetro de tipo não fosse fornecido, @typeReferencePathOf() poderia resolver para undefined ou, alternativamente, a string vazia "" . Se um tipo for fornecido, mas não tiver um caminho interno, ele será resolvido como "" .

Vantagens desta abordagem:

  • Não requer genéricos de tipo superior.
  • Não requer a permissão de literais como parâmetros genéricos.
  • Não requer extensão de genéricos de forma alguma.
  • Permite o preenchimento automático ao digitar a expressão typeof e usa um mecanismo existente de verificação de tipo para validá-lo em vez de precisar convertê-lo de uma representação de string em tempo de compilação.
  • Também pode ser usado em classes genéricas.
  • Não requer typeon (mas pode suportá-lo).
  • Pode ser aplicado em outros cenários.

+1

Agradável! Já comecei a escrever uma grande proposta para resolver o mesmo problema, mas encontrei essa discussão, então vamos discutir aqui.

Aqui está um trecho do meu problema não enviado:

Pergunta : Como a função a seguir deve ser declarada em .d.ts para ser usada em contextos onde deve ser usada?

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 função (com pequenas variações) pode ser encontrada em quase todas as bibliotecas "utils"

Existem dois casos de uso distintos de mapValues na selva:

uma. _Object-as-a-_ _Dictionary _ onde os nomes das propriedades são dinâmicos, os valores têm o mesmo tipo e a função é, em geral, monomórfica (ciente desse 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

b. _Object-as-a-_ _Record _ onde cada propriedade tem seu próprio tipo, enquanto a função é parametricamente polimórfica (por implementação)

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"

A melhor instrumentação disponível para mapValues com o TypeScript atual é:

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

Não é difícil ver que essa assinatura corresponde perfeitamente ao caso de uso _Object-as-a-_ _Dictionary _, enquanto produz um resultado 1 um tanto surpreendente da inferência de tipo quando aplicada no caso em que _Object-as-a-_ _Registro _ era a intenção.

O tipo {p1: T1, p2: T2, ...pn: Tn} será coagido para {[key: string]: T1 | T2 | ... | Tn } fundindo todos os tipos e descartando efetivamente todas as informações sobre a estrutura de um registro:

  • o correto e esperar 2 a ficar bem console.log(res.a[0].toFixed(2)) será rejeitada pelo compilador;
  • o console.log((<number>res['a'][0]).toFixed(2)) aceito tem dois locais não controlados e sujeitos a erros: o cast de tipo e o nome da propriedade arbitrária.

1, 2 - para os programadores que migram do ES para o TS


Passos possíveis para resolver este problema (e resolver outros também)

1. Apresente registros variados com a ajuda de tipos de literal de 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. Permitir que tipos formais sejam subscritos com outros tipos formais que são restritos para estender 'string'

exemplo:


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 a desestruturação de tipos formais

exemplo:

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

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

com todas as três etapas juntas, poderíamos escrever

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

Eu sei que a sintaxe é mais detalhada do que a proposta por @fdecampredon
mas não consigo imaginar como expressar o tipo de mapValues ou combineReducers com a proposta original.

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

com minha proposta, sua declaração será semelhante a:

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

É expressável com a proposta original?

@spion , @fdecampredon ?

@Artazor Definitivamente uma melhoria. Agora parece ser bom o suficiente para expressar meu caso de uso de benchmark deste recurso:

Assumindo que user.id e user.name são tipos de coluna de banco de dados contendo number e string ou seja, Column<number> e Column<string>

Escreva o tipo de função select que leva:

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

e retorna Query<{id: number; name: string}>

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

Não tenho certeza de como isso será verificado e inferido. Parece que pode haver um problema, pois o tipo T não existe. O tipo de retorno precisaria ser expresso em uma forma desestruturada? ie

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

@Artazor esqueceu de perguntar, o parâmetro p extends string realmente necessário?

Algo assim não seria suficiente?

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

ou seja, para todas as variáveis ​​de tipo T [p] em {...p:Column<T[p]>}

editar: outra pergunta, como isso será verificado quanto à exatidão?

@spion eu tenho seu ponto de vista - você me entendeu mal (mas é minha culpa), por T[p] eu quis dizer uma coisa totalmente diferente, e você verá que p extends string é uma coisa crucial aqui. Deixe-me explicar meus pensamentos em profundidade.

Vamos considerar quatro casos de uso para estruturas de dados: List , Tuple , Dictionary e Record . Vamos nos referir a eles como _estruturas de dados conceituais_ e descrevê-los com as seguintes propriedades:

Estruturas de dados conceituaisAcesso por
uma. posição
(número)
b. chave
(corda)
Contagem de
Itens
1. variável
(a mesma função para cada item)
ListaDicionário
2. fixo
(cada item desempenha sua própria função única)
TuplaRegistro

Em JavaScript, esses quatro casos são apoiados por duas formas de armazenamento: um Array - para a1 e a2 , e um Object - para b1 e b2 .

_Nota 1 _. Para ser honesto, não é correto dizer que Tuple é apoiado pelo Array , pois o único tipo de tupla "verdadeiro" é o tipo do objeto arguments não é um Array . No entanto, eles são mais ou menos intercambiáveis, especialmente no contexto da desestruturação e do Function.prototype.apply que abre a porta para a meta-programação. O TypeScript também aproveita Arrays para modelar Tuplas.

_Nota 2 _. Enquanto o conceito completo do Dicionário com chaves arbitrárias foi introduzido décadas mais tarde na forma de ES6 Map , a decisão inicial de Brendan Eich de fundir Record e Dictionary limitado (com apenas chaves de string) sob o mesmo capô de Object (onde obj.prop é equivalente a obj["prop"] ) tornou-se o elemento mais controverso de toda a linguagem (na minha opinião).
É a maldição e a bênção da semântica JavaScript. Isso torna a reflexão trivial e encoraja os programadores a alternar livremente entre os níveis de _programação_ e _meta-programação_ a um custo mental quase zero (mesmo sem perceber!). Acredito que seja a parte essencial do sucesso do JavaScript como linguagem de script.

Agora é a hora de o TypeScript fornecer a maneira de expressar tipos para essa semântica estranha. Quando pensamos no nível de _programação_, tudo está bem:

Tipos no nível de programaçãoAcesso por
uma. posição
(número)
b. chave
(corda)
Contagem de
Itens
1. variável
(a mesma função para cada item)
T []{[chave: string]: T}
2. fixo
(cada item desempenha sua própria função única)
[T1, T2, ...]{chave1: T1, chave2: T2, ...}

No entanto, quando mudamos para o nível de _metaprogramação_, então as coisas que foram corrigidas no nível de _programação_ repentinamente tornam-se variáveis! E aqui de repente reconhecemos as relações entre tuplas e assinaturas de função e propomos coisas como # 5453 (Tipos Variadic) que, na verdade, cobre apenas uma pequena (mas muito importante) parte das necessidades de metaprogramação - a concatenação de assinaturas, dando a capacidade de desestruturar parâmetro em tipos de rest-args:

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

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

No caso, se # 6018 também for implementado, a classe Function pode ser semelhante a

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

É ótimo, mas de qualquer forma incompleto.

Por exemplo, imagine que queremos atribuir um tipo correto à seguinte função:

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

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

Com os tipos variáveis ​​propostos, não podemos transformar os tipos da assinatura. Aqui precisamos de algo mais poderoso. Na verdade, é desejável ter funções de tempo de compilação que possam operar sobre tipos (como no Fluxo do Facebook). Porém, tenho certeza de que isso será possível apenas quando (e se) o sistema de tipos do TypeScript for estável o suficiente para expô-lo aos programadores finais sem risco significativo de quebrar o domínio do usuário em uma próxima atualização secundária. Portanto, precisamos de algo menos radical do que o suporte completo de meta-programação, mas ainda capaz de lidar com os problemas demonstrados.

Para resolver esse problema, quero apresentar a noção de _assinatura de objeto_. Aproximadamente

  1. o conjunto de nomes de propriedades do objeto, ou
  2. o conjunto de chaves do dicionário, ou
  3. o conjunto dos índices numéricos de uma matriz
  4. o conjunto de posições numéricas de uma tupla.

Idealmente, seria bom ter tipos literais inteiros, bem como tipos literais de string para representar assinaturas de matrizes ou tuplas como

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

as regras para literais inteiros são congruentes com as literais de string;

Então, podemos introduzir a sintaxe de abstração de assinatura, que corresponde exatamente à sintaxe de repouso / propagação do objeto:

{...Props: T }

com uma exceção significativa: aqui Props é o identificador que está vinculado ao quantificador 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[]

assim, ele é introduzido no escopo léxico do tipo e pode ser usado em qualquer lugar neste escopo como o nome do tipo. No entanto, seu uso é duplo: quando usado no lugar do nome da propriedade de repouso / propagação, ele representa a abstração sobre a assinatura do objeto, quando é usado um tipo autônomo, ele representa um subtipo da string (ou número, respectivamente).

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

E aqui está a parte mais sofisticada: _Tipos Principais Dependentes_

apresentamos construção de tipo especial: T for Prop (gostaria de usar esta sintaxe em vez de T [Prop] que o confundiu) onde Prop é o nome da variável de tipo que contém a abstração de assinatura do objeto. Por exemplo <Prop extends string, T for Prop> introduz dois tipos formais no tipo de escopo léxico, o Prop e o T onde é conhecido que para cada valor particular p de Prop haverá seu próprio tipo T .

Não dizemos que em algum lugar existe um objeto que possui propriedades Props e seus tipos são T ! Apresentamos apenas uma dependência funcional entre dois tipos. Tipo T é correlacionado com membros do tipo Props, e isso é tudo!

Isso nos dá a possibilidade de escrever coisas 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}>

entretanto, o segundo parâmetro é tratado não como um objeto, mas sim como o mapa abstrato de identificadores para tipos. Nesta perspectiva, T for P pode ser usado para representar sequências abstratas de tipos para tuplas quando P é um subtipo de número ( @JsonFreeman ?)

Quando T é usado em algum lugar dentro de {...P: .... T .... } ele representa exatamente um tipo particular daquele mapa.

É minha ideia principal.
Aguardo ansiosamente qualquer dúvida, pensamento, crítica -)

Certo, então extends string deve levar arrays (e argumentos variáveis) em consideração em um caso, e constantes de string (como tipos) no outro. Doce!

Não dizemos que em algum lugar existe um objeto que possui propriedades Props e seus tipos são T! Apresentamos apenas uma dependência funcional entre dois tipos. Tipo T é correlacionado com membros do tipo Props, e isso é tudo!

Eu não quis dizer isso, mas sim porque seus tipos são T [p], um dicionário de tipos indexado por p. Se essa for uma boa intuição, eu manteria.

No geral, porém, a sintaxe pode precisar de um pouco mais de trabalho, mas a ideia geral parece incrível.

É possível escrever unwrap para argumentos variados?

edit: nevermind, acabei de perceber que sua extensão proposta para tipos variadic aborda isso.

Olá pessoal,
Estou pensando em como resolver um dos meus problemas e encontrei esta discussão.
O problema é:
Eu tenho o método RpcManager.call(command:Command):Promise<T> e o uso seria assim:

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

Solução que eu acho que poderia ser:

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

Alguma opinião sobre isso?

@antanas-arvasevicius o último bloco de código nesse exemplo deve fazer o que você quiser.

parece que você tem mais uma questão de como realizar uma tarefa específica; use Stack Overflow ou registre um problema se você acha que encontrou um bug do compilador.

Olá, Ryan, obrigado por sua resposta.
Tentei o último bloco de código, mas não funciona.

Demonstração 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 não ter usado o Stack Overflow, mas pensei que estava relacionado a este problema :)
Devo abrir uma nova edição sobre este tópico?

Um parâmetro de tipo genérico não consumido impede que a inferência funcione; em geral você _nunca_ deve ter parâmetros de tipo não usados ​​em declarações de tipo, pois eles não têm sentido. Se você consumir T , tudo 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);

Isso é simplesmente incrível! Obrigado!
Não pensei que o parâmetro de tipo pudesse ser colocado dentro de tipo genérico e
TS irá extraí-lo.
Em 22 de fevereiro de 2016, 23:56, "Ryan Cavanaugh" [email protected] escreveu:

Um parâmetro de tipo genérico não consumido impede que a inferência funcione; dentro
geral, você _nunca_ deve ter parâmetros de tipo não utilizados no tipo
declarações, pois não têm sentido. Se você consumir T, tudo
trabalho:

interface de comando{foo: T} class MyCommand implementa Command <{status: string}> {foo: {status: string; }} classe RPC {chamada estática(comando: Comando): T {retorno; }}
deixe a resposta = RPC.call (new MyCommand ());
console.log (resposta.status);

-
Responda a este e-mail diretamente ou visualize-o no GitHub
https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -187404245
.

@ antanas-arvasevicius se você estiver criando APIs de estilo RPC, tenho alguns documentos que podem ser úteis: https://github.com/alm-tools/alm/blob/master/docs/contributing/ASYNC.md : rose:

As abordagens acima parecem:

  • bastante complicado;
  • não lide com o uso de strings no código. string não são "Encontrar todas as referências", nem refatoráveis ​​(por exemplo, renomeações).
  • alguns suportam apenas propriedades de "primeiro nível" e não expressões mais complexas (que são suportadas por alguns frameworks).

Aqui está outra ideia, inspirada por árvores de expressão C #. Esta é apenas uma ideia grosseira, nada totalmente pensado! A sintaxe é terrível. Só quero ver se isso inspira alguém.

Suponha que temos um tipo especial de strings para denotar expressões.
Vamos chamá-lo de type Expr<T, U> = string .
Onde T é o tipo de objeto inicial e U é o tipo de resultado.

Suponha que pudéssemos criar uma instância de Expr<T,U> usando um lambda que recebe um parâmetro do tipo T e executa um acesso de membro nele.
Por exemplo: person => person.address.city .
Quando isso acontece, todo o lambda é compilado para uma string contendo qualquer acesso ao parâmetro que era, neste caso: "address.city" .

Você pode usar uma string simples, que seria vista como Expr<any, any> .

Ter este tipo especial Expr na linguagem permite coisas como estas:

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 é basicamente uma forma limitada de para que as expressões são usadas em C #.
Você acha que isso poderia ser refinado e levar a algum lugar interessante?

@fdecampredon @RyanCavanaugh

_ ( @ jods4 - Lamento não estar respondendo à sua sugestão aqui. Espero que não fique 'enterrada' por meio dos comentários) _

Acho que a nomenclatura desse recurso ('Tipo de propriedade do tipo') é altamente confusa e muito difícil de entender. Foi _muito_ difícil até mesmo descobrir qual era o conceito descrito aqui e o que ele significa em primeiro lugar!

Primeiro, nem todos os tipos têm propriedades! undefined e null não (embora sejam apenas adições recentes ao sistema de tipos). Primitivos como number , string , boolean raramente são indexados por uma propriedade (por exemplo, 2 ["prop"]? Embora pareça funcionar, é quase sempre um erro)

Eu sugeriria nomear esse problema de referência de tipo de propriedade de interface por meio de valores literais de string . O tópico aqui não é sobre a introdução de um novo 'tipo', mas uma maneira muito particular de fazer referência a um existente usando uma variável de string ou parâmetro de função cujo valor _deve ser conhecido em tempo de compilação_.

Teria sido altamente benéfico se isso fosse descrito e exemplificado da forma mais simples possível, fora do contexto do caso de uso específico:

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 isso corretamente, o compilador deve saber com certeza que a string atribuída à variável não mudou entre o ponto em que foi inicializada e o ponto em que foi referenciada na expressão de tipo. Não acho isso muito fácil? Essa suposição sobre o comportamento do tempo de execução sempre seria válida? _

_Editar 2: Talvez isso funcione apenas com const quando usado com uma variável? _

Não tenho certeza se a intenção original era permitir que apenas a referência de propriedade no caso específico de um literal de string seja passado para um parâmetro de função ou método. por exemplo

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

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

Eu generalizei isso além do que originalmente pretendia?

Como isso lidaria com um caso em que o argumento da função não é uma string literal, ou seja, não é conhecido em tempo de compilação? por exemplo

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

Haverá um erro ou o padrão será any talvez?

(PS: Se essa era de fato a intenção, sugerirei renomear este problema para referência de tipo de propriedade de interface por função literal de string ou argumentos de método - por mais extenso que isso tenha se revelado, é muito mais preciso e explicativo do que o título atual).

Aqui está um exemplo de benchmark simples (o suficiente para uma ilustração) que mostra o que esse recurso precisa habilitar:

Escreva o tipo desta função, que:

  • pega um objeto contendo campos de promessa, e
  • retorna o mesmo objeto, mas com todos os campos resolvidos, cada campo tendo o tipo adequado.
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)
}

Quando chamado no seguinte objeto:

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

O resultado res deve ser do tipo {a: number; b: string}


Com esse recurso (totalmente desenvolvido por @Artazor), a assinatura seria

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

editar: corrigido o tipo de retorno acima, estava faltando Promise<...> ^^

@spion

Obrigado por tentar fornecer um exemplo, mas em nenhum lugar aqui eu encontrei uma especificação razoável e concisa e um conjunto de exemplos claros e reduzidos que são _desconectados_ de aplicações práticas e tento destacar a semântica e os vários casos extremos de como isso funcionaria, mesmo algo tão básico como:

function func<T extends object>(name: string): T[name] {
 ...
}
  1. Houve muito pouco esforço para fornecer um título eficaz e explicativo (como mencionei 'Tipo de propriedade do tipo' é um nome confuso e não muito explicativo para este recurso, que não é realmente sobre qualquer tipo em particular, mas sobre uma referência de tipo). Meu título mais sugerido foi _referência de tipo de propriedade de interface por função literal de string ou argumentos de método_ (presumindo que eu não entendi mal o escopo disso).
  2. Não houve uma menção clara do que aconteceria se name não fosse conhecido em tempo de compilação, nulo ou indefinido, por exemplo, func<MyType>(getRandomString()) , func<MyType>(undefined) .
  3. Não houve uma menção clara do que aconteceria se T fosse um tipo primitivo como number ou null .
  4. Nenhum detalhe claro foi fornecido sobre se isso se aplica apenas a funções e se T[name] pode ser usado no corpo da função. E, nesse caso, o que acontece se name for reatribuído no corpo da função e, portanto, seu valor não puder mais ser conhecido em tempo de compilação?
  5. Nenhuma especificação real de sintaxe: por exemplo, T["propName"] ou T[propName] funcionará também por conta própria? sem uma referência a um parâmetro ou variável, quero dizer - pode ser útil!
  6. Nenhuma menção ou discussão se T[name] pode ser usado em outros parâmetros ou mesmo fora dos escopos de função, por exemplo
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. Nenhuma discussão real sobre a solução alternativa relativamente simples usando um parâmetro genérico adicional e typeof (embora tenha sido mencionado, mas relativamente tarde após o recurso já ter sido aceito):

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'

Concluindo: não há uma proposta real aqui, apenas um conjunto de demonstrações de casos de uso. Estou surpreso que isso tenha sido aceito ou até mesmo recebido um interesse positivo pela equipe do TypeScript, porque de outra forma eu não acreditaria que isso estaria de acordo com seus padrões. Lamento se posso parecer um pouco duro aqui (não é típico de mim), mas nenhuma das minhas críticas é pessoal ou dirigida a qualquer indivíduo em particular.

Queremos realmente ir tão longe em termos de metaprogramação?

JS é uma linguagem dinâmica, o código pode manipular objetos de maneiras arbitrárias.
Existem limites para o que podemos modelar no sistema de tipos e é fácil criar funções que você não pode modelar no TS.
A questão é antes: até onde é razoável e útil ir?

O último exemplo ( awaitObject ) de @spion é ao mesmo tempo:

  • _Muito complexo_ do ponto de vista da linguagem (concordo com @malibuzios aqui).
  • _Muito estreito_ em termos de aplicabilidade. Esse exemplo escolhe restrições específicas e não generaliza bem.

BTW @spion, você não Promise .

Estamos longe do problema original, que era sobre a digitação de APIs que usam uma string que representa campos, como _.pluck
Essas APIs _devem_ ser suportadas porque são comuns e não tão complicadas. Não precisamos de meta-modelos como { ...p: T[p] } para isso.
Existem vários exemplos no OP, alguns mais de Aurelia em meu comentário em nameof issue .
Uma abordagem diferente pode cobrir esses casos de uso, consulte meu comentário acima para uma possível ideia.

@ jods4 não é nada estreito. Ele pode ser usado para várias coisas que são impossíveis de modelar no sistema de tipos no momento. Eu diria que é uma das últimas grandes peças que faltam no sistema de tipo TS. Existem muitos exemplos do mundo real acima por @Artazor https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -177287714 as coisas mais simples acima, o exemplo de "seleção" do construtor de consulta sql e assim por diante.

Este é awaitObject no bluebird. É um método real. O tipo é atualmente inútil.

Uma solução mais geral será melhor. Funcionará tanto para os casos simples quanto para os complexos. Uma solução insuficiente será encontrada em falta para metade dos casos e facilmente causará problemas de compatibilidade se for necessário adotar uma mais geral no futuro.

Sim, precisa de muito mais trabalho, mas também acho que @Artazor fez um excelente trabalho analisando e explorando todos os aspectos que não havíamos pensado antes. Eu não ficaria porque nos afastamos do problema original, só temos um melhor entendimento dele.

Podemos precisar de um nome melhor para isso, eu tentaria, mas geralmente não entendo essas coisas direito. Que tal "Genéricos de objeto"?

@spion , apenas para correção:

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

(você perdeu uma promessa na assinatura de saída)
-)

@ jods4 , concordo com @spion
Existem muitos problemas do mundo real onde { ...p: T[p] } é a solução. Para citar um: react + flux / redux

@spion @Artazor
Só estou preocupado porque isso está se tornando bastante complexo para especificar com precisão.

A motivação por trás desse problema mudou. Originalmente, tratava-se de apoiar APIs que aceitam uma string para denotar um campo de objeto. Agora ele está discutindo principalmente APIs que usam objetos como mapas ou registros de uma maneira muito dinâmica. Observe que, se pudermos matar dois coelhos com uma cajadada só, estou totalmente a favor.

Se voltarmos ao problema das APIs que pegam strings, o T[p] não é uma solução completa na minha opinião (eu disse o porquê antes).

_Só para correção_ awaitObject também deve aceitar propriedades não promissoras, pelo menos o Bluebird props aceita. Então agora temos:

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

Mudei o tipo de retorno para Promise<T> porque espero que essa notação funcione.
Existem outras sobrecargas, por exemplo, uma que leva uma promessa para tal objeto (sua assinatura é ainda mais divertida). Isso significa que a notação { ...p } precisa ser considerada uma combinação pior do que qualquer outro tipo.

Especificar tudo isso vai ser um trabalho árduo. Eu diria que é o próximo passo se você quiser levar isso adiante.

@spion @ jods4

Eu queria mencionar que esse recurso não se refere a genéricos e nem a tipos polimórficos de tipo superior. Esta é simplesmente uma sintaxe para fazer referência ao tipo de uma propriedade por meio de um tipo de conteúdo (com algumas extensões "avançadas" descritas a seguir), que não está muito longe de typeof no conceito. Exemplo:

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

Agora compare com:

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

let y: typeof x.abcd;

Existem duas diferenças principais com typeof . Ao contrário de typeof , isso se aplica a tipos (pelo menos os não primitivos, um fato que é freqüentemente omitido aqui) ao invés de instâncias. Além disso (e isso é importante), foi estendido para oferecer suporte ao uso de constantes de string literais (que devem ser conhecidas em tempo de compilação) como descritores de caminho de propriedade e genéricos:

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

No entanto, tecnicamente typeof poderia ter sido estendido para suportar literais de string (isso inclui o caso em que x tem um tipo genérico):

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

E com esta extensão, também poderia ser usado para resolver alguns dos casos-alvo aqui:

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

typeof entretanto, é mais poderoso, pois suporta uma quantidade indefinida de níveis de aninhamento:

let y: typeof x.prop.childProp.deeperChildProp

E este só vai um nível. Ou seja, não planejado (tanto quanto eu sei) para apoiar:

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

Acho que o escopo desta proposta (se isso pode ser chamado de uma proposta em seu nível de indefinição) é muito estreito. Pode ajudar a resolver um problema específico (embora possa haver soluções alternativas), o que parece deixar muitas pessoas ansiosas por promovê-lo. No entanto, também consome sintaxe valiosa da linguagem. Sem um plano mais amplo e uma abordagem orientada ao design, não parece sensato introduzir precipitadamente algo que, a partir desta data, nem mesmo tem uma especificação clara.

_Edits: corrigidos alguns erros óbvios nos exemplos de código_

Pesquisei um pouco sobre a alternativa typeof :

O suporte a idiomas futuros para typeof x["abcd"] e typeof x[42] foi aprovado e agora se enquadra no # 6606, que está atualmente em desenvolvimento (há uma implementação em funcionamento).

Isso vai até a metade. Com eles no lugar, o resto pode ser feito em vários estágios:

(1) Adicionar suporte para constantes literais de string (ou mesmo constantes numéricas - pode ser útil para tuplas?) Como especificadores de propriedade em expressões de tipo, por exemplo:

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

(2) Permitir a aplicação desses especificadores a tipos genéricos

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

(3) Permitir que esses especificadores sejam passados ​​e, na prática, "instanciar" as referências por meio de argumentos de função ( typeof list[0] está planejado, portanto, acredito que isso poderia abranger casos mais complexos 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 aqui é o proposto em # 1809)

Embora mais poderoso (por exemplo, pode suportar typeof obj[propName][nestedPropName] ), é possível que esta abordagem alternativa não cubra todos os casos descritos aqui. Por favor, deixe-me saber se há exemplos que não parecem ser tratados por isso (um cenário que vem à mente é quando a instância do objeto não é passada, no entanto, é difícil para mim neste ponto, imaginar um caso em que isso seria necessário, embora eu ache que seja possível).

_Edits: corrigidos alguns erros no código_

@malibuzios
Alguns pensamentos:

  • Isso corrige a definição da API, mas não ajuda o chamador. Ainda precisamos de algum tipo de nameof ou Expression no site da chamada.
  • Ele funciona para .d.ts definições, mas geralmente não será estaticamente inferível ou mesmo verificável em funções / implementações de TS.

Quanto mais penso nisso, mais Expression<T, U> parece a solução para esse tipo de API. Ele corrige os problemas do site de chamada, a digitação do resultado e pode ser deduzível + verificável em uma implementação.

@ jods4

Você mencionou em um comentário anterior acima que pluck pode ser implementado com expressões. Embora eu costumava ser muito familiarizado com o C #, nunca realmente experimentei com eles, então admito que não tenho um bom entendimento deles neste momento.

De qualquer forma, só queria mencionar que o TypeScript suporta _type argument inference_ , então em vez de usar pluck , pode-se apenas usar map e obter o mesmo resultado, que nem mesmo exigiria a especificação de um parâmetro genérico ( é inferido) e também forneceria verificação completa de tipo e conclusão:

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

Onde map (altamente simplificado para demonstração) é declarado como

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

Esse mesmo padrão de inferência pode ser usado como um 'truque' para passar qualquer resultado de um lambda arbitrário (que nem mesmo precisa ser chamado) para uma função e definir seu tipo de retorno para ela:

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: pode parecer bobagem passar em um lambda aqui e não só a coisa em si! entretanto, em casos mais complexos, como objetos aninhados (por exemplo, x.prop.Childprop ), podem haver referências indefinidas ou nulas que podem gerar erros. Tê-lo em um lambda, que não é necessariamente chamado, evita isso._

Admito que não estou muito familiarizado com alguns dos casos de uso discutidos aqui. Pessoalmente, nunca senti necessidade de chamar uma função que recebe um nome de propriedade como string. Passar um lambda (onde as vantagens que você descreve também são válidas) em combinação com interface de argumento de tipo geralmente é o suficiente para resolver muitos problemas comuns.

A razão pela qual sugeri a abordagem alternativa typeof foi principalmente porque ela parece abranger os mesmos casos de uso e fornecer a mesma funcionalidade para o que as pessoas descrevem aqui. Eu mesmo usaria? Não sei, nunca senti uma necessidade real (pode ser simplesmente que eu quase nunca uso bibliotecas externas como o sublinhado, quase sempre desenvolvo funções utilitárias sozinho, em uma base necessária).

@malibuzios Verdade, pluck é um pouco bobo com lambdas + map agora integrados.

Neste comentário , apresento 5 APIs diferentes, todas com strings - todas conectadas à observação de mudanças.

@ jods4 e outros

Queria apenas mencionar que quando # 6606 estiver completo, apenas implementando o estágio 1 (permitir que literais de string constantes sejam usados ​​como especificadores de propriedade), algumas das funcionalidades necessárias aqui podem ser alcançadas, embora não tão elegantemente quanto poderia (pode exigir o nome da propriedade a ser declarado como uma constante, adicionando uma instrução 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);

No entanto, a quantidade de esforço para implementar isso pode ser significativamente menor do que implementar os estágios 2 e 3 também (não tenho certeza, mas é possível que 1 já esteja coberto por # 6606). Com o estágio 2 implementado, isso também cobriria o caso em que x tem um tipo genérico (mas não tenho certeza se isso é realmente necessário?).

Edit: O motivo pelo qual usei uma constante externa e não apenas escrevi o nome da propriedade duas vezes foi não apenas para reduzir a digitação, mas também para garantir que os nomes sempre correspondessem, embora isso ainda não possa ser usado com ferramentas como "renomear" e "localizar tudo referências ", o que considero uma séria desvantagem.

@ jods4 Ainda estou procurando uma solução melhor que não faça uso de strings. Tentarei pensar no que pode ser feito com nameof e suas variantes.

Aqui está outra ideia.

Uma vez que os literais de string já são suportados como tipos, por exemplo, já se pode escrever:

let x: "HELLO"; 

Pode-se ver uma string literal passada para uma função como uma forma de especialização genérica

(_Editar: estes exemplos foram corrigidos para garantir que s seja imutável no corpo da função, embora const não seja compatível com posições de parâmetro neste ponto (não tenho certeza sobre readonly Apesar)._) :

function func(const s: string)

O tipo de parâmetro string associado a s pode ser visto como um genérico implícito aqui (já que pode ser especializado em literais de string). Para maior clareza, vou escrever de forma mais explícita (embora não tenha certeza se realmente é necessário):

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

poderia se especializar internamente em:

function func(s: "TEST")

E agora isso pode ser usado como uma maneira alternativa de "passar" o nome da propriedade, que eu sinto que captura melhor a semântica aqui.

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

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

observeProperty(x, nameof(x.age))

Como em T[S] , tanto T quanto S são parâmetros genéricos. Parece mais natural ter dois tipos combinados em uma posição de tipo do que misturar elementos de tempo de execução e de escopo de tipo (por exemplo, T[someString] ).

_Edits: exemplos reescritos para ter um tipo de parâmetro de string imutável._

Como estou usando TS 1.8.7 e não a versão mais recente, não sabia que nas versões mais recentes em

const x = "Hello";

O tipo de x já está inferido como o tipo literal "Hello" , (ou seja: x: "Hello" ), o que faz muito sentido (consulte # 6554).

Então, naturalmente, se houvesse uma maneira de definir um parâmetro como const (ou talvez readonly também funcionasse?):

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

Então eu acredito que isso poderia segurar:

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

Como parte disso é bastante novo e depende de recursos de linguagem recentes, tentarei resumi-lo da forma mais clara possível:

(1) Quando uma variável de string const (e talvez readonly também?) Recebe um valor que é conhecido em tempo de compilação, a variável recebe automaticamente um tipo literal que tem o mesmo valor ( Eu acredito que este é um comportamento recente que não acontece em 1.8.x ), por exemplo

const x = "ABCD";

O tipo de x é inferido como sendo "ABCD" , não string !, Por exemplo, pode-se afirmar isso como x: "ABCD" .

(2) Se readonly parâmetros de função fossem permitidos, um parâmetro readonly com um tipo genérico iria naturalmente especializar seu tipo para uma string literal quando for recebido como um argumento, pois o parâmetro é imutável!

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

Aqui S foi resolvido para o tipo "ABCD" , não string !

Porém, se o parâmetro str não fosse imutável, o compilador não pode garantir que ele não seja reatribuído no corpo da função, portanto, o tipo inferido seria apenas string .

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

func("ABCD");

(3) É possível tirar vantagem disso e modificar a proposta original para que o especificador de propriedade seja uma referência a um tipo , que precisaria ser restringido para ser uma derivada de string (em alguns casos, pode até ser apropriado restringi-lo apenas a tipos literais singleton, embora atualmente não haja uma maneira de fazer isso), em vez de uma entidade de tempo de execução:

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

Chamar isso não requer a especificação explícita de quaisquer argumentos de tipo, pois o TypeScript oferece suporte à inferência de argumento 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'

_Edits: corrigidos alguns erros de ortografia e código._
_Observação: uma versão generalizada e estendida desta abordagem modificada agora também é rastreada em # 7730._

Aqui está outro uso do tipo de propriedade type (ou "genéricos indexados" como gosto de chamá-los recentemente) que surgiu em uma discussão com @Raynos

Já podemos escrever o seguinte verificador geral para matrizes:

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

Depois de indexar os genéricos, também seríamos capazes de escrever um verificador geral 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 com os verificadores primitivos para string, number, boolean, null e undefined permitiriam que você escrevesse coisas como estas:

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

e ter o resultado com o verificador de tempo de execução correto _e_ protetor de tipo de tempo de compilação, ao mesmo tempo :)

Alguém já trabalhou nisso ou está no radar de alguém? Esta será uma grande melhoria para quantas bibliotecas padrão podem usar tipificações, incluindo os gostos de lodash ou ramda e muitas interfaces de banco de dados.

@malibuzios acredito que você está se aproximando da sugestão de @Artazor :)

Para abordar suas preocupações:

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

Este seria

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

Dessa forma, o nome seria bloqueado para o tipo mais específico (um tipo de constante de string) quando chamado com:

func("test")

O tipo S torna-se "test" (não string ). Como tal, str não pode ser reatribuído a um valor diferente. Se você tentou isso, por exemplo

str = "other"

O compilador pode produzir um erro (a menos que haja problemas de estabilidade de variação;))

Em vez de apenas obter o tipo de uma propriedade, gostaria de ter a opção de obter um supertipo arbitrário de tipo.

Portanto, minha sugestão é adicionar a seguinte sintaxe: em vez de apenas ter T[prop] , gostaria de adicionar a sintaxe T[...props] . Onde props deve ser uma matriz de membros de T . E o tipo resultante é um supertipo de T com membros de T definidos em props .

Acho que seria muito útil no Sequelize - um ORM popular para node.js. Por motivos de segurança e de desempenho, é aconselhável consultar apenas os atributos de uma tabela que você precisa usar. Isso geralmente significa supertipo de um 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

(No meu exemplo inclui o operador memberof , que é o que você espera que seja, e também a expressão options.attributes que é o mesmo que typeof options.attributes , mas acho que o typeof operador é redundante neste caso, porque está em uma posição que espera um tipo.).

Se ninguém insistir, comecei a trabalhar nisso.

O que você pensa sobre segurança de tipo dentro da função, ou seja, garantir que uma instrução de retorno retorne algo atribuível a um 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] ?
}

Além disso, o nome "Tipo de propriedade" usado aqui parece um pouco incorreto. Ele meio que afirma que o tipo tem propriedades, que quase todos os tipos têm.

Que tal "Tipo de propriedade referenciada"?

O que você acha sobre a segurança de tipos dentro da função

Meu brainstorming:

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
}

E proíba a string normal na linha de retorno.

O problema de

Algum comentário da equipe de TS sobre https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -239653337?

@RyanCavanaugh @mhegazy etc?

Em relação ao nome, comecei a chamar esse recurso (pelo menos a forma que @Artazor propôs) de "Genéricos Indexados"

Uma solução de outro ângulo de visão poderia ser para este problema. Não tenho certeza se já foi trazido, é uma longa discussão. Desenvolvendo uma sugestão genérica de string, poderíamos estender a assinatura de indexação. Como os literais de string podem ser usados ​​para o tipo de indexador, poderíamos fazer com que fossem equivalentes (como eu sei que não são no momento):

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

Então, poderíamos apenas escrever então

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

Existem algumas coisas que precisam ser consideradas:

  • como indicar que P só pode ser uma string literal?

    • uma vez que string estende tecnicamente todos os literais de string, P extends string não seria muito ideomático

    • outra discussão está acontecendo sobre a permissão de restrição P super string (# 7265, # 6613, https://github.com/Microsoft/TypeScript/issues/6613#issuecomment-175314703)

    • realmente precisamos nos preocupar com isso? podemos usar o que for aceitável para o tipo de indexador - se T tiver um indexador de string s ou number s. então P pode ser string ou number .

  • atualmente, se passarmos "something" como um segundo argumento, ele será do tipo string

    • literais de string # 10195 podem ser a resposta

    • ou o TS pode inferir que um P mais específico deve ser usado se for aceitável

  • indexadores são implicitamente opcionais { [i: string]: number /* | undefined */ }

    • como garantir se realmente não queremos ter undefined no domínio?

  • a inferência automática de tipo para todas as relações entre P , T e R é a chave para fazê-lo funcionar

@weswigham @mhegazy , e tenho discutido isso recentemente; avisaremos você sobre quaisquer desenvolvimentos que encontrarmos e tenha em mente que isso é apenas um protótipo da ideia.

Ideias atuais:

  • Um operador keysof Foo para obter a união dos nomes das propriedades de Foo como tipos de literal de string.
  • Um tipo Foo[K] que especifica que para algum tipo K que é um tipo de literal de string ou união de tipos de literais de string.

A partir desses blocos básicos, se você precisar inferir um literal de string como o tipo apropriado, você pode escrever

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

Aqui, K será um subtipo de keysof T que significa que é um tipo literal de string ou uma união de tipos literais de string. Qualquer coisa que você passar para o parâmetro key deve ser contextualmente digitada por esse literal / união de literais e inferido como um tipo de literal de string singleton.

Por exemplo

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

O maior problema é que keysof frequentemente precisa "atrasar" sua operação. Ele está muito ansioso para avaliar um tipo, o que é um problema para parâmetros de tipo como no primeiro exemplo que postei (ou seja, o caso que realmente queremos resolver é na verdade a parte difícil: sorria :).

Espero que isso dê a vocês uma atualização.

@DanielRosenwasser Obrigado pela atualização. Acabei de ver @weswigham enviar um PR sobre a operadora keysof , então talvez seja melhor passar esse problema para vocês.

Eu só me pergunto por que você decidiu se afastar da sintaxe original proposta?

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

e apresentar keysof ?

T[prop] é menos geral e requer muitas máquinas entrelaçadas. Uma grande questão aqui é como você relacionaria o conteúdo literal de prop aos nomes de propriedade de T . Eu nem tenho certeza do que você faria. Você adicionaria um parâmetro de tipo implícito? Você precisaria alterar o comportamento de digitação contextual? Você precisaria adicionar algo especial às assinaturas?

A resposta provavelmente é sim para todas essas coisas. Eu nos afastei disso porque meu instinto me disse que era melhor usar dois conceitos separados e mais simples e construir a partir daí. A desvantagem é que há um pouco mais de clichê em certos casos.

Se houver bibliotecas mais novas que usam esses tipos de padrões e esse clichê está dificultando a escrita em TypeScript, talvez devamos considerar isso. Mas, de modo geral, esse recurso tem como objetivo principal servir aos consumidores da biblioteca, porque o site de uso é onde você obtém os benefícios de qualquer maneira.

@DanielRosenwasser Mal desceu pela toca do coelho. Ainda não consigo encontrar problemas para implementar a ideia @SaschaNaz ? Acho que keysof é redundante neste caso. T[p] já relata que p deve ser um dos adereços literais de T .

Meu pensamento inicial de implementação foi introduzir um novo tipo chamado PropertyReferencedType .

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

Ao inserir uma função declarada com um tipo de retorno que é PropertyReferencedType ou inserir uma função que faz referência a PropertyReferencedType : Um tipo de ElementAccessExpression será aumentado com uma propriedade que faz referência a símbolo da propriedade acessada.

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

Portanto, um tipo com um símbolo de propriedade referenciado pode ser atribuído a PropertyReferencedType . Durante a verificação, referencedProperty deve corresponder a p em T[p] . Além disso, o tipo pai de uma expressão de acesso ao elemento deve ser atribuível a T . E para tornar as coisas mais fáceis, p também deve ser const.

O novo tipo PropertyReferencedType só existe dentro da função como um "tipo não resolvido". No site de chamada, é necessário resolver o tipo com p :

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

A PropertyReferencedType apenas se propaga por meio de atribuições de função e não pode se propagar por meio de expressões de chamada, porque PropertyReferencedType é apenas um tipo temporário destinado a ajudar na verificação do corpo de uma função com tipo de retorno T[p] .

Se você introduzir keysof e T[K] operadores do tipo, isso significa que poderíamos usá-los desta forma:

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 seu exemplo não teria o mesmo significado com meu

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

Não estou vendo como a assinatura seria escrita para _.pick do Underscore:

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

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

@rtm Eu sugeri isso em https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -234724380. Embora seja melhor abrir um novo problema, mesmo que esteja relacionado a este.

Implementação agora disponível em # 11929.

Esta página foi útil?
0 / 5 - 0 avaliações