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.
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
prop
Obviamente, a constante deve ser de um tipo que possa ser usado como tipo de índice ( string
, number
, Symbol
).
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
.
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?
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.
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 qualmembertypeof 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
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:
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:
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.typeon
(mas pode suportá-lo).+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:
console.log(res.a[0].toFixed(2))
será rejeitada pelo compilador;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
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
exemplo:
declare function mapValues<p extends string, T1[p], T2[p]>(
obj:{...p: T1[p]}, fn:(arg: T1[p]) => T2[p]
): {...p: T2[p]};
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 conceituais | Acesso por | ||
---|---|---|---|
uma. posição (número) | b. chave (corda) | ||
Contagem de Itens | 1. variável (a mesma função para cada item) | Lista | Dicionário |
2. fixo (cada item desempenha sua própria função única) | Tupla | Registro |
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 peloArray
, pois o único tipo de tupla "verdadeiro" é o tipo do objetoarguments
não é umArray
. No entanto, eles são mais ou menos intercambiáveis, especialmente no contexto da desestruturação e doFunction.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 fundirRecord
eDictionary
limitado (com apenas chaves de string) sob o mesmo capô deObject
(ondeobj.prop
é equivalente aobj["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ção | Acesso 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
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ãoT
! 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:
string
não são "Encontrar todas as referências", nem refatoráveis (por exemplo, renomeações).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:
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] {
...
}
name
não fosse conhecido em tempo de compilação, nulo ou indefinido, por exemplo, func<MyType>(getRandomString())
, func<MyType>(undefined)
.T
fosse um tipo primitivo como number
ou null
.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?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!T[name]
pode ser usado em outros parâmetros ou mesmo fora dos escopos de função, por exemplofunction 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:
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:
nameof
ou Expression
no site da chamada..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:
P
só pode ser uma string literal?P extends string
não seria muito ideomáticoP super string
(# 7265, # 6613, https://github.com/Microsoft/TypeScript/issues/6613#issuecomment-175314703)T
tiver um indexador de string
s ou number
s. então P
pode ser string
ou number
."something"
como um segundo argumento, ele será do tipo string
P
mais específico deve ser usado se for aceitável{ [i: string]: number /* | undefined */ }
undefined
no domínio?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:
keysof Foo
para obter a união dos nomes das propriedades de Foo
como tipos de literal de string.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.
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:
keysof Foo
para obter a união dos nomes das propriedades deFoo
como tipos de literal de string.Foo[K]
que especifica que para algum tipoK
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
Aqui,
K
será um subtipo dekeysof 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âmetrokey
deve ser contextualmente digitada por esse literal / união de literais e inferido como um tipo de literal de string singleton.Por exemplo
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.