Typescript: Sugestão: permitir que os acessadores get/set sejam de tipos diferentes

Criado em 26 mar. 2015  ·  125Comentários  ·  Fonte: microsoft/TypeScript

Seria ótimo se houvesse uma maneira de relaxar a restrição atual de exigir que os acessadores get/set tenham o mesmo tipo. isso seria útil em uma situação como esta:

class MyClass {

    private _myDate: moment.Moment;

    get myDate(): moment.Moment {
        return this._myDate;
    }

    set myDate(value: Date | moment.Moment) {
        this._myDate = moment(value);
    }
}

Atualmente, isso não parece ser possível, e eu tenho que recorrer a algo assim:

class MyClass {

    private _myDate: moment.Moment;

    get myDate(): moment.Moment {
        return this._myDate;
    }

    set myDate(value: moment.Moment) {
        assert.fail('Setter for myDate is not available. Please use: setMyDate() instead');
    }

    setMyDate(value: Date | moment.Moment) {
        this._myDate = moment(value);
    }
}

Isso está longe de ser o ideal, e o código seria muito mais limpo se tipos diferentes fossem permitidos.

Obrigado!

Design Limitation Suggestion Too Complex

Comentários muito úteis

Um getter e setter JavaScript com tipos diferentes é perfeitamente válido e funciona muito bem e acredito que seja a principal vantagem/propósito desse recurso. Ter que fornecer um setMyDate() apenas para agradar o TypeScript estraga tudo.

Pense também em bibliotecas JS puras que seguirão este padrão: os .d.ts terão que expor uma união ou any .

O problema é que os acessadores não aparecem em .d.ts de forma diferente das propriedades normais

Em seguida, essa limitação deve ser corrigida e esse problema deve permanecer em aberto:

// MyClass.d.ts

// Instead of generating:
declare class MyClass {
  myDate: moment.Moment;
}

// Should generate:
declare class MyClass {
  get myDate(): moment.Moment;
  set myDate(value: Date | moment.Moment);
}

// Or shorter syntax:
declare class MyClass {
  myDate: (get: moment.Moment, set: Date | moment.Moment);
  // and 'fooBar: string' being a shorthand for 'fooBar: (get: string, set: string)'
}

Todos 125 comentários

Eu posso ver como isso seria bom aqui (e isso foi solicitado antes, embora eu não consiga encontrar o problema agora), mas não é possível se o utilitário é ou não suficiente para garantir isso. O problema é que os acessadores não são exibidos em .d.ts de forma diferente das propriedades normais, uma vez que parecem iguais a partir dessa perspectiva. Significando que não há diferenciação entre o getter e o setter, portanto não há como a) exigir que uma implementação use um acessador em vez de um único membro de instância e b) especificar a diferença de tipos entre o getter e o setter.

Obrigado pela rápida resposta, Dan. Vou seguir o caminho menos elegante. Obrigado pelo ótimo trabalho!

Um getter e setter JavaScript com tipos diferentes é perfeitamente válido e funciona muito bem e acredito que seja a principal vantagem/propósito desse recurso. Ter que fornecer um setMyDate() apenas para agradar o TypeScript estraga tudo.

Pense também em bibliotecas JS puras que seguirão este padrão: os .d.ts terão que expor uma união ou any .

O problema é que os acessadores não aparecem em .d.ts de forma diferente das propriedades normais

Em seguida, essa limitação deve ser corrigida e esse problema deve permanecer em aberto:

// MyClass.d.ts

// Instead of generating:
declare class MyClass {
  myDate: moment.Moment;
}

// Should generate:
declare class MyClass {
  get myDate(): moment.Moment;
  set myDate(value: Date | moment.Moment);
}

// Or shorter syntax:
declare class MyClass {
  myDate: (get: moment.Moment, set: Date | moment.Moment);
  // and 'fooBar: string' being a shorthand for 'fooBar: (get: string, set: string)'
}

Eu percebo que isso é apenas uma opinião, mas escrever um setter tal que a.x === y não seja true imediatamente após a.x = y; é uma bandeira vermelha gigante. Como os consumidores de uma biblioteca como essa sabem quais propriedades têm efeitos colaterais mágicos e quais não têm?

Como [você] sabe quais propriedades têm efeitos colaterais mágicos e quais não têm?

Em JavaScript pode ser pouco intuitivo, em TypeScript as ferramentas (IDE + compilador) irão reclamar. Por que amamos o TypeScript novamente? :)

Um getter e setter JavaScript com tipos diferentes é perfeitamente válido e funciona muito bem e acredito que seja a principal vantagem/propósito desse recurso.

Isso está argumentando que o JavaScript é fracamente tipado, então o TypeScript deve ser fracamente tipado. :-S

Isso está argumentando que o JavaScript é fracamente tipado, então o TypeScript deve ser fracamente tipado

C# permite isso e isso não faz com que essa linguagem seja fracamente tipada C# não permite get/set de tipos diferentes.

C# permite isso e isso não faz com que essa linguagem seja fracamente tipada C# não permite get/set de tipos diferentes.

:piscadela:

Ninguém está argumentando (até onde eu posso ver) que os acessadores devem ser fracamente tipados, eles estão argumentando que devemos ter flexibilidade para definir o(s) tipo(s).

Muitas vezes é necessário copiar um objeto antigo simples para instâncias de objetos.

    get fields(): Field[] {
      return this._fields;
    }

    set fields(value: any[]) {
      this._fields = value.map(Field.fromJson);
    }

Isso é muito melhor do que a alternativa e permite que meu setter encapsule o tipo exato de setters lógicos para os quais os setters são feitos.

@paulwalker o padrão que você usa lá (um setter pegando any e um getter retornando um tipo mais específico) é válido hoje.

@danquirk Ótimo saber, obrigado! Parece que eu só precisava atualizar meu compilador de plugins IDE para ST.

@danquirk Isso parece não funcionar de acordo com o playground (ou versão 1.6.2):
http://www.typescriptlang.org/Playground#src =%0A%0Aclass%20Foo%20%7B%0A%0A%20%20get%20items()%3A%20string%5B%5D%20%7B%0A %09%20%20retorno%20%5B%5D%3B%0A%20%20%7D%0A%20%20%0A%20%20conjunto%20itens(valor%3A%20qualquer)%20%7B%0A% 09%20%20%0A%20%20%7D%0A%7D

Acabei de testar com typescript@next (Versão 1.8.0-dev.20151102), e também tenho um erro.

~$ tsc --version
message TS6029: Version 1.8.0-dev.20151102
~$ cat a.ts
class A {
    get something(): number {return 5;}
    set something(x: any) {}
}

~$ tsc -t es5 a.ts
a.ts(2,2): error TS2380: 'get' and 'set' accessor must have the same type.
a.ts(3,2): error TS2380: 'get' and 'set' accessor must have the same type.

Ironicamente, depois de atualizar meu linter Sublime, ele não apresentou mais um erro, que está usando o TypeScript 1.7.x. Eu tenho assumido que é um aprimoramento futuro em 1.7+, então talvez 1.8.0 regrediu.

Mesmo com a versão do código do visual studio (0.10.5 (dezembro de 2015)) que suporta o typescript 1.7.5 e com o typescript 1.7.5 instalado globalmente na minha máquina, isso ainda é um problema:

image

Então, qual versão isso será suportado?
Obrigado

Acho que Dan se enganou. O getter e o setter devem ser de tipo idêntico.

vergonha. teria sido um bom recurso para escrever objetos de página para uso em testes de transferidor.

Eu teria sido capaz de escrever um teste de transferidor:

po.email = "[email protected]";
expect(po.email).toBe("[email protected]");

... criando um objeto de página:

class PageObject {
    get email(): webdriver.promise.Promise<string> {
        return element(by.model("ctrl.user.email")).getAttribute("value")
    }
    set email(value: any) {
        element(by.model("ctrl.user.email")).clear().sendKeys(value);
    }
}

E esse código requer que o setter seja do tipo any ?

O getter retornará um webdriver.promise.Promise<string> mas o setter eu quero atribuir um string .

Talvez a seguinte forma mais longa do teste do transferidor torne isso mais claro:

po.email = "[email protected]";
var currentEmail : webdriver.promise.Promise<string> = po.email;
expect(currentEmail).toBe("[email protected]")

@RyanCavanaugh Com a introdução de anotações nulas, isso impede o código que permite chamar um setter com nulo para defini-lo com algum valor padrão.

class Style {
    private _width: number = 5;

    // `: number | null` encumbers callers with unnecessary `!`
    get width(): number {
        return this._width;
    }

    // `: number` prevents callers from passing in null
    set width(newWidth: number | null) {
        if (newWidth === null) {
            this._width = 5;
        }
        else {
            this._width = newWidth;
        }
    }
}

Você poderia considerar pelo menos permitir que os tipos sejam diferentes na presença de | null e | undefined ?

Isso seria realmente um bom recurso.

Então, isso será um recurso?

@artyil está fechado e marcado como _By Design_, o que indica que atualmente não há planos para adicioná-lo. Se você tiver um caso de uso convincente que acha que substitui as preocupações expressas acima, sinta-se à vontade para apresentar seu caso e comentários adicionais podem fazer a equipe principal reconsiderar sua posição.

@kitsonk Acho que casos de uso mais do que suficientes foram fornecidos nos comentários acima. Embora o design atual siga um padrão comum da maioria das outras linguagens tipadas com essa restrição, ele é desnecessário e excessivamente restritivo no contexto do Javascript. Embora seja por design, o design está errado.

Eu concordo.

Depois de pensar um pouco mais sobre isso. eu acho que a questão aqui é realmente a complexidade da implementação. Pessoalmente, acho o exemplo de @Arnavion atraente, mas o sistema de tipos hoje trata getters/setters como propriedades regulares. para que isso funcione, ambos devem ter o mesmo valor. para suportar tipos de leitura/gravação seria uma grande mudança, não tenho certeza se o utilitário aqui valeria o custo de implementação.

Embora eu ame o TypeScript e aprecie todo o esforço que a equipe coloca nele (sério, vocês são demais!), devo admitir que estou desapontado com essa decisão. Seria uma solução muito melhor do que as alternativas de getFoo()/setFoo() ou get foo()/set foo()/setFooEx() .

Apenas uma pequena lista de problemas:

  • Atualmente, assumimos que as propriedades têm exatamente um tipo. Agora precisaríamos distinguir entre o tipo "ler" e o tipo "escrever" de cada propriedade em cada local
  • Todos os relacionamentos de tipo se tornam substancialmente mais complexos porque temos que raciocinar sobre dois tipos por propriedade em vez de um ( { get foo(): string | number; set foo(): boolean } pode ser atribuído a { foo: boolean | string | number } ou vice-versa?)
  • Atualmente, assumimos que uma propriedade, depois de definida, ainda tem o tipo do valor definido na linha a seguir (o que aparentemente é uma suposição errada nas bases de código de algumas pessoas, wat?). Provavelmente teríamos que "desligar" qualquer análise de controle de fluxo em propriedades como esta

Honestamente, eu realmente tento evitar ser prescritivo aqui sobre como escrever código, mas eu realmente me oponho à ideia de que este código

foo.bar = "hello";
console.log(foo.bar);

deve imprimir qualquer coisa que "hello" em uma linguagem que tente ter uma semântica sã. As propriedades devem ter um comportamento indistinguível de campos para observadores externos.

@RyanCavanaugh , embora eu concorde com você na opinião injetada, posso ver um contra-argumento de que _pode_ ser muito TypeScripty ... Jogando algo fracamente tipado em um setter, mas tendo algo sempre fortemente tipado retornado, por exemplo:

foo.bar = [ '1', 2 ];  // any[]
console.log(foo.bar);  // number[]: [ 1, 2 ]

Embora pessoalmente eu tenha a tendência de pensar que se você vai ser tão mágico, é melhor criar um método para que o desenvolvedor final possa entender claramente o que será dobrado, dobrado e mutilado.

Aqui está nosso caso de uso para esse recurso. Nossa API introduziu um recurso que chamamos de _Autocasting_. O principal benefício é uma experiência de desenvolvedor simplificada que pode eliminar o número de classes a serem importadas para atribuir propriedades para as quais o tipo está bem definido.

Por exemplo, uma propriedade de cor pode ser expressa como uma instância Color ou como uma string CSS como rgba(r, g, b, a) ou como uma matriz de 3 ou 4 números. A propriedade ainda é digitada como instância de Color , pois é o tipo do que você obtém ao ler o valor.

Algumas informações sobre isso: https://developers.arcgis.com/javascript/latest/guide/autocasting/index.html

Nossos usuários ficaram muito felizes em obter esse recurso, reduzindo o número de importações necessárias, e estão entendendo perfeitamente que o tipo muda a linha após a atribuição.

Outro exemplo para este problema: https://github.com/gulpjs/vinyl#filebase

file.base = 'd:\\dev';
console.log(file.base); //  'd:\\dev'
file.base = null;
console.log(file.base); //  'd:\\dev\\vinyl' (returns file.cwd)

Então o setter é string | null | undefined e o getter é apenas string . Que tipo devemos usar nas definições de tipo para esta biblioteca? Se usarmos o primeiro, o compilador exigiria verificações nulas inúteis em todos os lugares. Se usarmos o último, não poderemos atribuir null a esta propriedade.

Eu tenho outro exemplo em que gostaria que o getter retornasse anulável, mas onde o setter nunca deve permitir null como entrada, estilizado como:

class Memory {
    public location: string;
    public time: Date;
    public company: Person[];
}

class Person
{
    private _bestMemoryEver: Memory | null;

    public get bestMemoryEver(): Memory | null { // Might not have one yet
        return this._bestMemoryEver;
    }

    public set bestMemoryEver(memory: Memory) { // But when he/she gets one, it can only be replaced, not removed
        this._bestMemoryEver = memory;
    }
}

var someDude = new Person();
// ...
var bestMemory: Memory | null = someDude.bestMemoryEver;
//...
someDude.bestMemoryEver = null; // Oh no you don't!

Eu entendo que pode ser muito trabalhoso construir alguma lógica especial para permitir que getters/setters difiram em null, e não é um grande problema para mim, mas seria bom ter.

@Elephant-Vessel filosoficamente eu adoro o exemplo, ele representa bem a natureza inconstante dos seres humanos, mas não estou convencido de que não representaria isso ainda melhor, permitindo null (ou undefined ) a ser definido. Como posso modelar a falha de sinapse no sistema?

@aluanhaddad Por que você quer falha de sinapse? Não quero esse tipo de coisa ruim no meu universo ;)

Alguma atualização sobre isso? Que tal ter um valor padrão quando definido como nulo ou indefinido?

Não quero que o consumidor tenha que verificar o null antes. Atualmente, tenho que tornar o setter um método separado, mas gostaria que fossem iguais.

Abaixo está o que eu gostaria de ter:

export class TestClass {
  private _prop?: number;

  get prop(): number {
    // return default value if not defined
    this._prop === undefined ? 0 : this._prop;
  }
  set prop(val: number | undefined) {
    this._prop = val;
  }
}

Parece-me que ter o benefício da verificação não nula vem com pegadinhas, e essa é uma delas. Com a verificação nula estrita desativada, isso é possível, mas você não obtém a ajuda do compilador para evitar exceções de referência nula. No entanto, se você quiser assistência do compilador, acho que deve vir com mais suporte, como ter definições separadas para getters e setters com relação a pelo menos nulidade, se nada mais.

Os rótulos sobre o problema indicam que é uma limitação de design e a implementação seria considerada muito complexa, o que significa essencialmente que, se alguém tiver uma razão super convincente para que isso aconteça, não vai a lugar nenhum.

@mhegazy @kitsonk Posso ser tendencioso, mas sinto que este é um bug que apareceu para verificação nula estrita em um padrão comum, especialmente em outras chaves como linguagens onde eles ainda não têm verificação nula. Uma solução alternativa exigiria que o consumidor usasse o operador bang ou verificasse se ele nunca é realmente nulo (que é o ponto de nunca ser nulo com o uso de valores padrão).

Isso é interrompido quando você adiciona a verificação nula estrita porque agora são tipos tecnicamente diferentes. Não estou pedindo que tipos diferentes fortes sejam definidos, mas parece que os requisitos de design para habilitar isso também permitiriam tipos diferentes fortes.

Um design alternativo poderia ser usado para tipos fracos, de modo que tipos como null e undefined seriam especialmente encamisados ​​para definições de interface e arquivos d.ts se eles não quisessem habilitar tipos totalmente diferentes.

Em resposta a https://github.com/Microsoft/TypeScript/issues/2521#issuecomment -199650959
Aqui está um design de proposta que deve ser menos complexo de implementar:

export interface Test {
  undefset prop1: number; // property [get] type number and [set] type number | undefined
  nullset prop2: number; // property [get] type number and [set] type number | null
  nilset prop3: number; // property [get] type number and [set] type number | null | undefined
  undefget prop4: number; // property [get] type number | undefined and [set] type number
  nullget prop5: number; // property [get] type number | null and [set] type number
  nilget prop6: number; // property [get] type number | null | undefined and [set] type number
}

Parece que há algumas pessoas assistindo a este tópico que estão muito mais familiarizadas com o TypeScript, então talvez alguém que ainda esteja prestando atenção possa responder a uma pergunta relacionada. Nesta questão do Cesium eu mencionei a limitação do tipo get/set que estamos discutindo aqui, e o pessoal do Cesium disse que o padrão que eles estão seguindo vem do C# -- é o construtor implícito .

O TypeScript pode oferecer suporte a construtores implícitos? Ou seja, posso dizer que myThing.foo sempre retorna um Bar , e pode ser atribuído um Bar diretamente, mas também pode ser atribuído um number que ser silenciosamente envolvido / usado para inicializar um Bar , como uma conveniência para o desenvolvedor? Se for possível fazer isso anotando Bar , ou talvez dizendo especificamente que " number é atribuível a Bar<number> ", isso resolveria o caso de uso discutido na questão do Cesium, e também muitas das questões levantadas neste tópico.

Se não, preciso sugerir suporte ao construtor implícito em um problema separado?

Quanto mais eu penso / leio sobre isso, mais tenho certeza de que o "padrão construtor implícito" precisará do recurso descrito nesta edição. A única maneira possível no vanilla JS é usar acessadores get/set de objeto, porque essa é a única vez que a atribuição nominal (operador = ) está realmente chamando uma função definida pelo usuário. (Certo?) Então, acho que realmente precisaremos

class MyThing{
  set foo(b: Bar<boolean> | boolean);
  get foo(): Bar<boolean>;
}

o que parece que @RyanCavanaugh acha que não é "semântica sã".

O simples fato é que existe uma biblioteca JS bastante popular por aí que usa esse padrão, e parece que é difícil, se não impossível, descrever as restrições de TS existentes. Espero estar errado.

JavaScript já permite o padrão que você descreve. O _desafio_ é que o lado de leitura e gravação dos tipos no TypeScript são, por design, considerados iguais. Houve uma modificação na linguagem para não permitir a atribuição ( readonly ), mas há alguns problemas que solicitaram o conceito _write only_, que foram discutidos como muito complexos para pouco valor no mundo real.

IMO, desde que o JavaScript permitiu acessores, as pessoas criaram APIs potencialmente confusas com eles. Pessoalmente, acho confuso que algo na atribuição _magicamente_ mude para outra coisa. Qualquer coisa implícita, especialmente conversão de tipo, é a ruína do JavaScript IMO. É exatamente a flexibilidade que causa problemas. Com esse tipo de conversão, onde há _magic_, eu pessoalmente gosto de ver métodos sendo chamados, onde fica um pouco mais explícito para o consumidor que algum tipo de ✨ irá ocorrer e fazer um getter read only para recuperar valores.

Isso não significa que o uso do mundo real não exista, isso é potencialmente sensato e racional. Acho que isso se deve à complexidade de dividir todo o sistema de tipos em dois, onde os tipos precisam ser rastreados em suas condições de leitura e gravação. Isso parece um cenário muito não trivial.

Eu concordo com o :sparkles: aqui, mas não estou argumentando a favor ou contra o uso do padrão, estou apenas tentando acompanhar o código que já o usa e descrever a forma dele. Já funciona do jeito que funciona, e TS não está me dando as ferramentas para descrevê-lo.

Para o bem ou para o mal, o JS nos deu a capacidade de transformar atribuições em chamadas de função e as pessoas o estão usando. Sem a capacidade de atribuir diferentes tipos a pares set/get , por que ainda ter set/get em tipos de ambiente? Salsa não precisa saber que uma propriedade é implementada com um getter e um setter se for sempre tratar myThing.foo como uma variável de membro de um único tipo, independentemente de qual lado da atribuição está. (Obviamente, a compilação real do TypeScript é outra coisa.)

@thw0rted

Parece que há algumas pessoas assistindo a este tópico que estão muito mais familiarizadas com o TypeScript, então talvez alguém que ainda esteja prestando atenção possa responder a uma pergunta relacionada. Nesta questão do Cesium eu mencionei a limitação do tipo get/set que estamos discutindo aqui, e o pessoal do Cesium disse que o padrão que eles estão seguindo vem do C# -- é o construtor implícito.

Os operadores de conversão implícitos definidos pelo usuário do C# executam a geração de código estático com base nos tipos de valores. Os tipos TypeScript são apagados e não influenciam o comportamento do tempo de execução ( async / await casos de borda para Promise polyfillers não obstante).

@kitsonk eu tenho que discordar em geral com relação às propriedades. Tendo passado um bom tempo com Java, C++ e C#, eu absolutamente amo propriedades (como visto em C#) porque elas fornecem uma abstração sintática crítica (isso é verdade em JavaScript). Eles permitem que as interfaces segreguem recursos de leitura/gravação de maneira significativa sem usar a sintaxe de alteração. Eu odeio ver verbos desperdiçados em operações triviais como getX() quando obter X pode ser implícito.
APIs confusas da IMO, muitas das quais fazem como você diz abusar de acessadores, decorrem mais de muitos _setters_ fazerem coisas mágicas.

Se eu tiver uma visualização somente leitura, mas ao vivo sobre alguns dados, digamos registry , acho que as propriedades somente leitura são muito fáceis de entender.

interface Entry {key: string; value: any;}

export function createRegistry() {
  let entries: Entry[] = [];
  return {
    register(key: string, value: any) {
      entries = [...entries, {key, value}];
    },
    get entries() {
      return [...entries];
    }
  }
}

const registry = createRegistry();

registry.register('hello', '您好');
console.log(registry.entries); //[{key: 'hello', value: '您好'}]
registry.register('goodbye', '再见');
console.log(registry.entries); //[{key: 'hello', value: '您好'}, {key: 'goodbye', value: '再见'}]

Desculpe pela tangente, mas eu adoro acessórios para isso e acho que eles são fáceis de entender, mas estou disposto a ser convencido do contrário e a legibilidade é minha primeira preocupação.

Quando o TypeScript limita o JavaScript, isso se torna mais um incômodo do que uma vantagem. O TypeScript não foi feito para ajudar os desenvolvedores a se comunicarem?

Além disso, os setters são chamados de Mutadores por um motivo. Se eu não precisasse de nenhum tipo de conversão, eu não usaria um setter, eu definiria a variável sozinho.

portar projeto javascript para typescript. encontrei esse problema..

Isso seria bom para usar em decoradores @Input angulares. Como o valor é passado de um modelo, ficaria muito mais limpo, na minha opinião, lidar com diferentes tipos de objetos recebidos.

Atualização: isso parece funcionar para mim

import { Component, Input } from '@angular/core';
import { flatMap, isString, isArray, isFalsy } from 'lodash';

@Component({
  selector: 'app-error-notification',
  templateUrl: './error-notification.component.html',
})

export class ErrorNotificationComponent {
  private _errors: Array<string> = [];
  constructor() { }
  /**
   * 'errors' is expected to be an input of either a string or an array of strings
   */
  @Input() set errors(errors: Array<string> | any){
      // Caller just passed in a string instead of an array of strings
      if (isString(errors)) {
        this._errors = [errors];
      }
      // Caller passed in array, assuming it is a string array
      if (isArray(errors)) {
        this._errors = errors;
      }
      // Caller passed in something falsy, which means we should clear error list
      if (isFalsy(errors)) {
        this._errors = [];
      }
      // At this point just set it to whatever might have been passed in and let
      // the user debug when it is broken.
      this._errors = errors;
  }

  get errors() {
    return this._errors;
  }
}

Então estávamos nós? Eu realmente gostaria de ter a possibilidade de retornar um tipo diferente com o getter do que com o setter. Por exemplo:

class Field {
  private _value: string;

  get value(): string {
    return this._value;
  }

  set value(value: any) {
    this._value = String(value);
  }
}

Isso é o que faz 99% das implementações nativas (se você passar um número para um (input as HTMLInputElement).value ele sempre retornará uma string. Na verdade, get e set devem ser considerados como métodos e devem permitir alguns:

set value(value: string);
set value(value: number);
set value(value: any) {
  this._value = String(value);
}
  // AND/OR
set value(value: string | number) {
  this._value = String(value);
}

@raysuelzer , quando você diz que seu código "funciona", ErrorNotificationComponent.errors não retorna um tipo de Array<string> | any ? Isso significa que você precisa de protetores de tipo toda vez que usá-lo, quando você sabe que ele só pode retornar Array<string> .

@lifaon74 , até onde eu sei, não há movimento sobre esse assunto. Acho que um caso convincente foi feito - vários cenários apresentados, toneladas de código JS legado que não pode ser descrito adequadamente no Typescript por causa disso - mas o problema está encerrado. Talvez se você acha que os fatos no terreno mudaram, abra um novo? Eu não conheço a política da equipe sobre recauchutar velhos argumentos, mas eu apoio você.

Eu não conheço a política da equipe sobre recauchutar velhos argumentos, mas eu apoio você.

Eles vão reconsiderar assuntos previamente encerrados. Fornecer um 👍 no início da edição dá credibilidade ao fato de que é significativo. Acredito que geralmente caiu na categoria "muito complexo" porque significaria que o sistema de tipos teria que ser bifurcado em cada leitura e gravação e suspeito que o sentimento seja o esforço e o custo de colocar isso para saciar o que é um caso de uso válido, mas um tanto incomum, não vale a pena.

Meu sentimento pessoal é que seria um _bom ter_, especialmente por ser capaz de modelar código JavaScript existente que usa esse padrão de forma eficaz.

Eu, pessoalmente, consideraria o assunto resolvido se tivéssemos alguma solução alternativa não totalmente onerosa para o problema de JS legado. Ainda sou meio que um novato em TS, então talvez minha solução atual (conversão forçada ou guardas de tipo desnecessários) não seja um uso ideal dos recursos existentes?

Concordar. O setter será muito limitado quando o tipo tiver que ser o mesmo... Por favor, melhore.

Procurando recomendações sobre como fazer isso:

get price() {
    return (this._price as number);
  }

  set price(price) {
    this._price = typeof price === 'string' ? parseFloat(parseFloat(price).toFixed(8)) : parseFloat(price.toFixed(8));
  }

Eu quero que o valor armazenado seja um número, mas quero converter de string para float no setter para não precisar analisarFloat toda vez que defino uma propriedade.

Eu acho que o veredicto é que este é um padrão Javascript válido (ou pelo menos razoavelmente bem aceito), que não se encaixa bem com os internos do Typescript. Talvez se mudar os internos for uma mudança muito grande, poderíamos obter algum tipo de anotação que altera a forma como o TS compila internamente? Algo como

class Widget {
  get price(): number | /** <strong i="7">@impossible</strong> */ string | undefined { return this._price; }
  set price(val: number|string|undefined){ ... }
}

let w = new Widget();
w.price = 10;
// Annotation processes as "let p:number|undefined = (w.price as number|undefined)"
let p: number|undefined = w.price;

Em outras palavras, talvez possamos marcar o TS de forma que o pré-processador (?) converta todas as leituras de w.price em uma conversão explícita, antes que o TS seja transpilado para JS. Claro, eu não conheço os detalhes de como o TS gerencia as anotações, então isso pode ser um lixo total, mas a ideia geral é que talvez fosse mais fácil de alguma forma magickk o TS em outro TS, do que mudar como o transpilador TS gera JS.

Não estou dizendo que esse recurso não é necessário, mas aqui está uma maneira de contornar set . IMO get deve sempre retornar o mesmo tipo de variável, então isso foi bom o suficiente para mim.

class Widget {
    get price(): number { return this._price; }
    set price(val){ return this.setPrice(val); } // call another function

    // do processing here
    private setPrice(price: number | string): number {
        let num = Number(price);
        return isNaN(num) ? 0 : num;
    }
}

@iamjoyce widget.price = '123' emite o erro de compilação no seu código

@iamjoyce => errado porque o compilador assume val: number em set price(val)

Sim, também estou aqui porque o uso de componentes do Angular parece querer que tenhamos diferentes tipos de getter/setter.

Angular sempre injetará uma string se você definir uma propriedade de componentes (através de um atributo) da marcação (a menos que use uma expressão de ligação) independentemente do tipo da propriedade de entrada. Então com certeza seria bom poder modelar isso assim:

private _someProperty: SomeEnum;
set someProperty(value: SomeEnum | string) {
   this._someProperty = this.coerceSomeEnum(value);
} 
get someProperty(): SomeEnum {
  return this._someProperty;
}

Hoje, isso funciona se deixarmos de fora o | string mas para tê-lo descreveria com mais precisão como o Angular acaba usando o componente. Nesse caso, se você acessá-lo a partir do código, o tipo da propriedade importa, mas se você defini-lo como um atributo, ele forçará uma string.

Eu não acho que geralmente queremos essa funcionalidade porque gostaríamos de projetar APIs dessa maneira. Concordo que as propriedades são melhores se não causarem efeitos colaterais de coerção por baixo das cobertas. Para contornar isso, seria ótimo se o Angular tivesse sido projetado de modo que os conjuntos de atributos versus conjuntos de propriedades de associação entrassem em diferentes pontos de entrada.

Mas se olharmos para isso pragmaticamente, é útil se o TypeScript nos permitir modelar as interações com bibliotecas externas que estão usando nossos tipos de uma maneira que desafie o axioma de igualdade de tipo de leitura/gravação.

Nesse caso, seria altamente problemático usar o tipo de união no getter, pois exigiria todos os tipos de guardas/asserções de tipo irritantes. Mas deixar o tipo de união fora do setter parece errado, pois qualquer ferramenta pode decidir no caminho começar a tentar verificar se as propriedades que estão sendo definidas a partir de atributos devem ser atribuíveis a partir da string.

Portanto, neste caso, o sistema de tipos não é expressivo o suficiente para capturar como uma biblioteca externa tem permissão para usar o tipo. Isso não importa _imediatamente_, pois a biblioteca externa não consome realmente esses tipos no nível do typescript, com informações de tipo. Mas pode eventualmente importar, pois o ferramental pode muito bem consumir as informações de tipo.

Uma pessoa acima mencionou uma solução alternativa que parece um pouco desagradável, mas provavelmente poderia funcionar, por meio da qual poderíamos usar o tipo de união para o setter/getter, mas ter alguma maneira de indicar que certos tipos da união são impossíveis para o getter. Assim, são, de fato, pré-removidos da consideração no fluxo de controle do site do site de chamada do getter como se alguém tivesse usado um tipo de guarda para verificar se eles não estão presentes. Isso parece irritante comparado a permitir uma união de superconjunto no setter em comparação com uma união de subconjunto no getter.

Mas talvez seja uma maneira de resolver as coisas internamente. Desde que o setter seja apenas uma união de superconjuntos do tipo getter. Trate os tipos do getter e do setter como equivalentes internamente, mas marque partes do tipo de união do getter como impossíveis. Para que a análise de fluxo de controle os remova de consideração. Isso contornaria as restrições de design?

Para detalhar o que foi dito acima. Talvez fosse útil, do ponto de vista da expressividade do tipo, poder indicar porções de um tipo composto como impossíveis. Isso não afetaria a igualdade com outro tipo com a mesma estrutura, mas sem os modificadores impossíveis. Um modificador impossível afetaria apenas a análise do fluxo de controle.

Como uma sugestão alternativa, se houvesse alguma sintaxe para aplicar uma proteção de tipo definida pelo usuário a um valor de retorno, isso também seria suficiente. Percebo que isso é, em circunstâncias normais, um pouco ridículo, mas ter esse tipo de expressividade no valor de retorno e nos argumentos ajudaria a resolver esses casos extremos.

O protetor de tipo definido pelo usuário no valor de retorno também tem o benefício de ser algo que pode ser expresso em declarações/interfaces de tipo quando compactado em uma propriedade?

Apenas adicionando outro caso de uso aqui, mobx-state-tree permite definir uma propriedade de duas maneiras diferentes (de instâncias e tipos de instantâneos), no entanto, ele só a retornará de uma única maneira padrão (instâncias) do acessador get , então isso seria extremamente útil se fosse suportado.

Estou contornando assim:

interface SomeNestedString {
  foo: string;
}

...

private _foo: SomeNestedString | string;

get foo(): SomeNestedString | string {
  return this._foo;
}

set foo(value: SomeNestedString | string) {
  this._foo = (value as SomeNestedString).foo;
}

Não acho que isso resolva o problema em questão. Você ainda precisaria de guardas de tipo ao usar o getter, embora, na realidade, o getter só retorne um subconjunto do tipo union.

Estou usando any para contornar o TSLint. E o compilador também não reclama.

export class FooBar {
  private bar: string;

  get foo (): string | any {
    return this.bar;
  }

  set foo (value: Date | string | any) {
    // Type guarding enables IntelliSense in VS Code
    if (value instanceof Date) {
      this.bar = value.toISOString();
    } else if (typeof value === 'string') {
      this.bar = value;
    } else {
      this.bar = String(value); // Or throw an error
    }
  }
}

@jpidelatorre desta forma você perde a segurança do tipo.

const fooBar = new FooBar()
const a: number = fooBar.foo // works while it should fail
fooBar.foo = 123 // fails only at runtime, not compile time. It doesnt fail in this particular case with strings and numbers, but it will with something more complex

@keenondrums É exatamente por isso que queremos que os acessadores tenham tipos diferentes. O que eu encontrei é uma solução alternativa, não uma solução.

@jpidelatorre minha solução atual é usar outra função como setter

export class FooBar {
  private bar: string;

  get foo (): string {
    return this.bar;
  }

  setFoo (value: Date | string ) {}
}

@keenondrums Não tão bonito quanto os acessórios, mas a melhor opção ainda.

Não é realmente um modelo para o que acontece com as propriedades @input em um componente Angular, a menos que você use uma função separada para um getter.

O que é muito feio.

Eu também gostaria desse recurso, mas preciso que funcione bem em arquivos .d.ts, onde não há soluções alternativas. Estou tentando documentar algumas classes expostas via Mocha (a ponte Objective-C/Javascript), e as propriedades de instância de itens encapsulados são configuradas assim:

class Foo {
    get bar:()=>number;
    set bar:number;
}

const foo = new Foo();
foo.bar = 3;
foo.bar(); // 3

Também vi muitos casos em que uma API permite definir uma propriedade com um objeto que corresponde a uma interface, mas o getter sempre retorna uma instância de classe real:

interface IFoo {
    bar: string;
}

class Foo implements IFoo {
    bar: string;
    toString():string;
}

class Example {
    get foo:Foo;
    set foo:Foo|IFoo;
}

Eu tinha esquecido em grande parte sobre esse problema, mas um pensamento me ocorreu enquanto lia sobre isso. Acho que chegamos à ideia de que vale a pena fazer isso em abstrato, mas tecnicamente complicado demais para ser viável. Esse é um cálculo de compensação - não é tecnicamente impossível , apenas não vale o tempo e o esforço (e complexidade de código adicional) para implementar. Certo?

O fato de que isso torna impossível descrever a API principal do DOM altera com precisão a matemática? @RyanCavanaugh diz

Eu realmente me oponho à ideia de que este código foo.bar = "hello"; console.log(foo.bar); deve imprimir qualquer coisa além de "olá" em uma linguagem que tenta ter uma semântica sã.

Poderíamos discutir se deveria ser usado dessa maneira, mas o DOM sempre suportou construções como el.hidden=1; console.log(el.hidden) // <-- true, not 1 . Este não é apenas um padrão que algumas pessoas usam, não é apenas que está em uma biblioteca popular, então pode ser uma boa ideia apoiá-lo. É um princípio central de como o JS sempre funcionou - um pouco de DWIM embutido na alma da linguagem - e torná-lo impossível no nível da linguagem quebra o princípio fundamental do TS, que deve ser um "superconjunto" do JS . É uma bolha feia saindo do diagrama de Venn, e não devemos esquecê-la.

É por isso que ainda gosto da ideia de manter o setter/getter com o mesmo tipo:

number | boolean

Mas introduzindo algum tipo de typegaurd onde você pode indicar que, enquanto o getter tecnicamente tem o mesmo tipo de união, na realidade ele só retornará um subconjunto dos tipos na união. Não tratá-lo como uma espécie de gaurd de estreitamento no getter (uma coisa de fluxo de inferência?) não o torna mais direto do que uma modificação no modelo de tipo? (Ele diz não saber nada sobre os internos...)

Isso poderia, alternativamente, estar implícito se o tipo usado no getter fosse um subconjunto estrito do tipo setters?

Isso poderia, alternativamente, estar implícito se o tipo usado no getter fosse um subconjunto estrito do tipo setters?

👍!

Acho que todos podemos concordar que você não deveria conseguir um tipo que não fosse configurável; Eu gostaria apenas de alguma maneira de restringir automaticamente o tipo em get para ser o tipo que eu sei que sempre será.

Eu não entendo por que alguns estão objetando que isso violaria a segurança do tipo e tudo mais. Não vejo que viole qualquer tipo de segurança se permitirmos que pelo menos o setter tenha um tipo diferente, porque de qualquer forma temos uma verificação em vigor que você não permitirá que outro tipo defina uma propriedade.
Ex:
Digamos que eu tenha uma propriedade como Arraymas do DB isso será retornado como uma string com separação por vírgula, como digamos '10,20,40'. Mas não posso mapear isso para a propriedade mode agora, então seria muito útil se você pudesse permitir como

private _employeeIdList: número[]

get EmployeeIDList(): number[] {
return this._employeeIdList ;
}
set EmployeeIDList(_idList: any ) {
if (typeof _idList== 'string') {
this._employeeIdList = _idList.split(',').map(d => Number(d));
}
else if (typeof _idList== 'object') {
this._employeeIdList = _idList as number[];
}
}

Eu teria resolvido facilmente esse problema e é perfeitamente seguro para tipos, mesmo que você permita um tipo diferente em SET, mas ainda assim nos impede de atribuir o tipo errado à propriedade. Então vença vença. Espero que os membros da equipe deixem seu ego de lado e tentem pensar no problema que ele cria e corrigir isso.

Eu gostaria de dizer que ainda acho que isso é realmente importante para modelar o comportamento das bibliotecas JS existentes.

Embora seja muito bom torcer o nariz e definir um setter que coage um tipo para um subconjunto do tipo de entrada e sempre retorna esse subconjunto no getter como um cheiro de código, na realidade, esse é um padrão razoavelmente comum na terra do JS .

Eu acho que o TypeScript é adorável, pois nos permite ser muito expressivos ao descrever as cagaries das APIs JS existentes, mesmo que elas não tenham APIs estruturadas de maneira ideal. Eu acho que este é um cenário onde se o TypeScript fosse expressivo o suficiente para nos permitir indicar que o getter sempre retornaria um tipo que é o subconjunto estrito do tipo do getter, isso adicionaria uma quantidade enorme de valor na modelagem de APIs existentes, mesmo as DOM!

Trusted Types é uma nova proposta de API do navegador para combater o DOM XSS. Já está implementado no Chromium (atrás de um sinalizador). A maior parte da API modifica os setters de várias propriedades DOM para aceitar tipos confiáveis. Por exemplo, .innerHTML aceita TrustedHTML | string enquanto sempre retorna string . Para descrever a API no TypeScript, precisaríamos que esse problema fosse corrigido.

A diferença dos comentários anteriores é que esta é uma API do navegador (e não uma biblioteca de usuário) que não pode ser alterada facilmente. Além disso, o impacto de alterar o tipo de Element.innerHTML para any (que é a única solução possível atualmente) é maior do que descrever imprecisamente uma biblioteca de usuário.

Existe uma chance de que este pull-request seja reaberto? Ou existem outras soluções que eu perdi?

Cc: @mprobst , @koto.

Em uma linguagem que oferece suporte à união de tipos, como o TypeScript, esse recurso é natural e um ponto de venda.

@RyanCavanaugh mesmo quando o getter e o setter têm o mesmo tipo, não é garantido que o.x === y depois o.x = y , pois o setter pode fazer alguma sanitização antes de salvar o valor.

element.scrollTop = -100;
element.scrollTop; // returns 0

Subscrevo a preocupação da @vrana. O comportamento atual impossibilita a modelagem de algumas das APIs existentes no Typescript.

Isso é especialmente verdadeiro para APIs da Web, para muitas das quais os tipos setter e getter são diferentes. Na prática, os setters de API da Web executam todos os tipos de coerção de tipo, alguns deles especificados diretamente para um determinado recurso do navegador, mas a maioria deles implicitamente via IDL . Muitos dos setters alteram o valor também, veja por exemplo a especificação da interface de localização . Este não é um único erro do desenvolvedor - é uma especificação da API que os desenvolvedores da web codificam.

Limitar os tipos getter permite que o Typescript represente essas APIs, o que agora é impossível.

Você pode representar essas APIs porque é correto dizer que o tipo da propriedade é a união dos tipos possíveis que você pode fornecer ao setter ou obter do getter.

Simplesmente não é _eficiente_ descrever uma API dessa maneira. Você está exigindo que o consumidor use um protetor de tipo em todas as instâncias em que eles usam o getter para restringir os tipos possíveis.

Tudo bem se você não tornou axiomático que você sempre retornará um tipo restrito de um getter, já que muitas APIs da Web bloqueiam suas especificações.

Mas mesmo deixando isso de lado por um momento, e falando sobre APIs de usuário, um forte caso de uso para aceitar tipos de união em um setter é o "scripty". Queremos aceitar uma variedade de tipos discretos que podemos coagir de forma aceitável para o tipo que realmente queremos.

Por que permitir isso? Fácil de usar.

Isso pode não importar para APIs que você está desenvolvendo internamente para uso de sua própria equipe, mas pode importar muito para APIs projetadas para consumo público e generalizado.

É adorável que o TypeScript possa nos permitir descrever com precisão uma API que relaxou os tipos aceitáveis ​​para algumas de suas propriedades, mas esse benefício é prejudicado pelo atrito na outra extremidade, onde a verificação/proteção excessiva de tipos é necessária para determinar o tipo de retorno do getter , que preferimos _especificar_.

Eu diria que é um cenário diferente do caso idealizado que @RyanCavanaugh postula. Esse caso implica que o getter e o setter devem sempre ter o mesmo tipo de união porque seu campo de apoio também tem o mesmo tipo de união, e você sempre fará a viagem de ida e volta desse valor, e alterar o tipo é simplesmente absurdo.

Acho que esse caso gira em torno de um uso mais idealizado de tipos de união, em que você está lidando com tipos construídos e realmente está tratando esse tipo de união como uma unidade semântica, para a qual você provavelmente deveria ter criado um alias.

type urlRep = string | Url;

A maioria das coisas vai apenas ida e volta, e lida apenas com adereços comuns, e em alguns casos você vai quebrar a caixa preta com alguns tipos de guardas.

Eu diria que é um cenário totalmente diferente do que estamos descrevendo aqui. O que estamos descrevendo é a realidade de que APIs de uso geral/público, especialmente para uso em linguagens de script como JavaScript, geralmente relaxam deliberadamente os tipos aceitáveis ​​para um setter, porque há uma variedade de tipos que eles concordam em coagir para o tipo ideal, então eles oferecem isso como uma melhoria da qualidade de vida para aceitar tudo isso.

Esse tipo de coisa pode parecer sem sentido se você for o produtor e o consumidor de uma API, pois isso só dá mais trabalho, mas pode fazer muito sentido se você estiver projetando uma API para consumo em massa.

E eu não acho que alguém iria querer que o setter/getter tivesse tipos _disjoint_. O que está sendo discutido aqui é que o produtor da API afirma ao consumidor da API que o getter retornará um valor com um tipo que é um subconjunto estrito do tipo de união do setter.

E eu não acho que alguém iria querer que o setter/getter tivesse tipos disjuntos.

Apenas para fornecer um ponto de vista contrário a isso. Eu definitivamente gostaria disso porque ter tipos disjuntos para setter/getter é completamente legal em javascript. E acho que o objetivo principal deveria ser tornar o sistema de tipos expressivo o suficiente para digitar corretamente qualquer coisa que seja legal em javascript (pelo menos no nível .d.ts). Eu entendo que não é uma boa prática, nem objetos globais, por exemplo, ou alterar o protótipo de builtins como função etc. podemos fazer isso (mesmo que isso faça com que alguns arquivos de definição de tipo mal projetados apareçam definitivamente digitados).

Acabei de me deparar com uma situação em que preciso que isso seja um recurso para ter os tipos corretos que não controlo. O setter precisa ser mais liberal e coagir a um tipo mais conservador que pode ser consistentemente retornado pelo getter.

Eu realmente gostaria que esse recurso existisse. Estou portando o código JavaScript para o TypeScript, e é uma pena ter que refatorar os setters se quiser segurança de tipo.

Por exemplo, eu tenho algo assim:

class Vector3 { /* ... */ }

type XYZ = Vector3 | [number, number, number] | {x: number, y: number, z: number}
type PropertyAnimator = (x: number, y: number, z: number, timestamp: number) => XYZ
type XYZSettables =  XYZ | PropertyAnimator

export class Transformable {
        // ...

        set position(newValue: XYZSettables) {
            this._setPropertyXYZ('position', newValue)
        }
        get position(): Vector3 {
            return this._props.position
        }

        // ...
}

E como você pode imaginar, o uso é muito flexível:

const transform = new Transformable

// use an array
transform.position = [20, 30, 40]

// use an object
transform.position = {y: 30, z: 40} // skip `x` this time

// animate manually, a property directly
requestAnimationFrame((time) => {
  transform.position.x = 100 * Math.sin(time * 0.001)
})

// animate manually, with an array, which could be shared across instances
const pos = [10, 20, 30]
requestAnimationFrame((time) => {
  pos[2] = 100 * Math.sin(time * 0.001)
  transform.position = pos
})

// Animate with a property function
transform.position = (x, y, z, time) => [x, y, 100 * Math.sin(time * 0.001)]

// or a simple increment:
transform.position = (x, y, z) => [x, y, ++z]

// etc

// etc

// etc

Isso tem 4 anos. Como ainda não foi corrigido? Esse recurso, como mencionado nos comentários acima, é significativo! Pelo menos considere reabrir o problema?

E eu concordo completamente com @kitsonk :

@kitsonk Acho que casos de uso mais do que suficientes foram fornecidos nos comentários acima. Embora o design atual siga um padrão comum da maioria das outras linguagens tipadas com essa restrição, ele é desnecessário e excessivamente restritivo no contexto do Javascript. Embora seja por design, o design está errado.

O TypeScript 3.6 introduziu a sintaxe para digitação de acessadores em arquivos de declaração , mantendo a restrição de que o tipo de getter e setter deve ser idêntico.

Nosso kit de ferramentas de interface do usuário depende muito dos setters de conversão automática, onde o setter aceita mais tipos do que o tipo singular retornado pelo getter. Portanto, seria bom se o TS oferecesse uma maneira de tornar esse padrão seguro para o tipo.

Parece que @RyanCavanaugh deu a refutação mais concreta desse recurso 3 anos atrás . Gostaria de saber se os casos de uso do DOM relatados recentemente, bem como a disponibilidade da nova sintaxe de declaração, podem permitir uma nova decisão?

Sim, assim que vi esse novo recurso, imediatamente pensei nessa solicitação de recurso também. Eu também quero isso para os propósitos de nossas estruturas de interface do usuário. Este é um caso de uso real, pessoal.

A TSConf 2019 será realizada no dia 11 de outubro, pensando que seria uma ótima retrospectiva na sessão de perguntas e respostas, se alguém tivesse a chance 🤔

Eu posso ter dito isso antes neste longo tópico, mas acho que vale a reiteração.

IMO, o sistema de tipos expressivos no TypeScript serve a dois propósitos distintos. A primeira é permitir que você escreva um novo código mais seguro. E se esse fosse o único propósito, talvez você pudesse argumentar que poderia evitar esse caso de uso, pois o código pode ser mais seguro se você não permitir esse cenário (mas acho que ainda poderíamos ter uma discussão sobre isso).

No entanto, o segundo objetivo é capturar o comportamento das bibliotecas como elas são e é uma prática razoavelmente comum no DOM e nas bibliotecas JS para auto-coagir no setter, mas retornar um tipo esperado no getter. Para nos permitir capturar isso no sistema de tipos, podemos descrever com mais precisão os frameworks existentes como eles são .

Pelo menos, acho que Design Limitation poderia ser removido aqui, não acho que isso pertença mais.

Esta aparentemente é a razão para o fato de que por #33749 .style.display = ...something nullable... não verifica mais; que é apresentado como um problema de correção de nulidade. Isso é ser um pouco desonesto, não é? É irritante ter que descobrir novas alterações importantes quando elas são rotuladas erroneamente como correções de bugs (eu procurei por problemas reais que isso pode ter causado). Pessoalmente, acho muito menos surpreendente que null tenha um comportamento especial "use default", do que a string vazia; e até o typecipt 3.7 eu preferia usar null para capturar isso. De qualquer forma, seria bom se anotações de tipo intencionalmente incorretas feitas para contornar essa limitação fossem claramente rotuladas como tal, para economizar tempo na triagem de problemas de atualização.

Também estou interessado em encontrar um caminho a seguir para isso. E se fosse permitido apenas em contextos ambientais? @RyanCavanaugh isso ajudaria a resolver suas preocupações de complexidade?

Meu caso de uso para isso é que tenho uma API onde um proxy retorna uma promessa, mas uma operação de conjunto não define uma promessa. Não posso descrever isso no TypeScript agora.

let post = await loadPost()
let user = await loadUser()
post.author = user // Proxy handles links these two objects via remote IDs
await save(post)

// Somewhere else in code
let post = await loadPost()
let author = await post.author

Seja funções nativas de correção de tipo ou proxies em geral, o TypeScript parece ser da opinião de que esses tipos de recursos foram um erro.

Eles realmente deveriam tirar os números 6 e 7 desta lista (https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals#goals)

Meu problema

Eu gostaria de enviar um item para uma matriz no método set e obter toda a matriz no método get. Por enquanto vou ter que usar set myArray(...value: string[]) que funciona bem. Aborrecido muito com este problema... por favor, considere remover isso. Uma informação ou aviso funcionaria bem para isso.

Exemplo (o que eu gostaria de fazer)

class MyClass {
   _myArray: string[] = [];

   set myArray(value: string) {
      this._myArray.push(value);
   }

   get myArray(): string[] {
      return this._myArray;
   }
}

Exemplo (o que devo fazer)

class MyClass {
   _myArray: string[] = [];

   set myArray(...value: string[]) {
      this._myArray.push(value[0]);
   }

   get myArray(): string[] {
      return this._myArray;
   }
}

Uma solução que encontrei quando precisei disso foi definir a propriedade union type . Isso ocorre porque meu tempo chega como uma string da API, mas preciso definir como um objeto Moment para minha interface do usuário.

Meu Exemplo

class TimeStorage {
    _startDate: string | Moment = ""
    _endDate: string | Moment = ""

    set startDate(date: Moment | string) {
        this._startDate = moment(date).utc()
    }
    get startDate(): Moment | string {
        return moment.utc(this._startDate)
    }

    set endDate(date: Moment) { 
        this._endDate = moment.utc(date)
    }
    get endDate() { 
        return moment.utc(this._endDate)
    }
}

Estou um pouco atrasado para a festa, mas recentemente me deparei com esse problema. É um dilema interessante, então eu brinquei com ele um pouco.

Vamos supor que isso é algo que queremos fazer, uma classe simples que tem uma propriedade que aceita strings e números (para qualquer finalidade), mas a propriedade é realmente armazenada como uma string:

class Foo {
    constructor() {
        this._bar = '';
        this.baz = '';
    }

    // error: 'get' and 'set' accessor must have the same type.(2380)
    get bar(): string {
        return this._bar;
    }

    // error: 'get' and 'set' accessor must have the same type.(2380)
    set bar(value: string | number) {
        this._bar = value.toString();
    }

    public baz: string;
    private _bar: string;
}

Como isso não é possível e ainda queremos uma digitação forte, precisamos de algum código extra. Agora vamos fingir para o propósito do nosso exemplo que alguém criou uma biblioteca Property que se parecia com isso:

/**
 * Abstract base class for properties.
 */
abstract class Property<GetType, SetType> {
    abstract get(): GetType;
    abstract set(value: SetType): void;
}

/**
 * Proxify an object so that it's get and set accessors are proxied to
 * the corresponding Property `get` and `set` calls.
 */
function proxify<T extends object>(obj: T) {
    return new Proxy<any>(obj, {
        get(target, key) {
            const prop = target[key];
            return (prop instanceof Property) ? prop.get() : prop;
        },
        set(target, key, value) {
            const prop = target[key];
            if (prop instanceof Property) {
                prop.set(value);
            } else {
                target[key] = value;
            }
            return true;
        }
    });
}

Usando esta biblioteca, poderíamos implementar nossa propriedade tipada para nossa classe assim:

class Bar extends Property<string, string | number> {
    constructor(bar: string | number) {
        super();
        this.set(bar);
    }

    get(): string {
        return this._bar;
    }

    set(value: string | number) {
        this._bar = value.toString();
    }

    private _bar: string;
}

class Foo {
    constructor() {
        this.bar = new Bar('');
        this.baz = '';
    }

    public bar: Bar;
    public baz: string;
}

E então, usar essa classe teria tipo getter e setter seguro para bar :

const foo = new Foo();

// use property's typed setter
foo.bar.set(42);
foo.baz = 'foobar';

// use property's typed getter
// output: 42 foobar
console.log(foo.bar.get(), foo.baz);

E se você precisar usar os setters ou getters de propriedade de forma mais dinâmica, você pode usar a função proxify para envolvê-lo:

const foo = new Foo();

// use proxified property setters
Object.assign(proxify(foo), { bar: 100, baz: 'hello world' });

// use property's typed getter
// output: 100 hello world
console.log(foo.bar.get(), foo.baz);

// use proxified property getters
// output: {"bar":"100","baz":"hello world"}
console.log(JSON.stringify(proxify(foo)));

Isenção de responsabilidade: Se alguém quiser pegar este código e colocá-lo em uma biblioteca ou fazer o que quiser com ele, sinta-se à vontade para fazê-lo. Eu sou muito preguiçoso.

Apenas algumas notas extras e então eu prometo que vou parar de enviar spam para essa longa lista de pessoas!

Se adicionarmos isso à biblioteca imaginária:

/**
 * Create a property with custom `get` and `set` accessors.
 */
function property<GetType, SetType>(property: {
    get: () => GetType,
    set: (value: SetType) => void
}) {
    const obj = { ...property };
    Object.setPrototypeOf(obj, Property.prototype);
    return obj;
}

Obtemos uma implementação um pouco mais próxima da classe original e, mais importante, tem acesso a this :

class Foo {
    constructor() {
        this._bar = '';
        this.baz = '';
    }

    public bar = property({
        get: (): string => {
            return this._bar;
        },
        set: (value: string | number) => {
            this._bar = value.toString();
        }
    });

    public baz: string;
    private _bar: string;
}

Não é a coisa mais bonita do mundo, mas funciona. Talvez alguém possa refinar isso e fazer algo que seja realmente legal.

No caso improvável de que qualquer pessoa lendo este tópico não esteja convencida de que esse recurso é importante, dou a você esse hack incrivelmente feio que o Angular acabou de introduzir :

Seria ideal mudar o tipo de valor aqui, de boolean para boolean|'', para corresponder ao conjunto de valores que são realmente aceitos pelo setter. O TypeScript requer que o getter e o setter tenham o mesmo tipo, portanto, se o getter retornar um booleano, o setter ficará preso ao tipo mais estreito .... Angular suporta a verificação de um tipo mais amplo e mais permissivo para @Input() do que é declarado para o próprio campo de entrada. Habilite isso adicionando uma propriedade estática com o prefixo ngAcceptInputType_ à classe do componente:

````
class botão Enviar {
private _disabled: boolean;

get disabled(): boolean {
return this._disabled;
}

set disabled(valor: boolean) {
this._disabled = (valor === '') || valor;
}

estático ngAcceptInputType_disabled: boolean|'';
}
````

Por favor, por favor , corrija isso.

Sim, nós literalmente temos essa feiúra em todo o lugar para apoiar os padrões que o Angular quer usar. A restrição atual é muito opinativa.

Se o TS quiser ser usado para modelar toda a amplitude de bibliotecas na web, então ele precisa se esforçar para ser menos opinativo, ou falhará em modelar todos os projetos.

Neste ponto, acredito que o ônus é dos contribuidores para comunicar por que esta edição não está sendo reaberta. Existe um membro contribuinte que seja fortemente contra isso neste momento?

Estamos construindo uma biblioteca DOM para NodeJs que corresponde à especificação W3C, mas esse problema do Typescript torna isso impossível. Qualquer atualização?

É chocante para mim que alguns membros da equipe principal do TypeScript sejam contra um recurso NECESSÁRIO para recriar uma das bibliotecas JavaScript mais usadas no planeta: o DOM.

Não há outra maneira de a) implementar muitas partes da especificação W3C DOM enquanto b) usar o sistema de tipos do TypeScript (a menos que você use any em todos os lugares, o que anula todo o propósito do TypeScript).

A página inicial de typescriptlang.org é atualmente imprecisa quando afirma:

TypeScript é um superconjunto tipado de JavaScript que compila para JavaScript simples.

A incapacidade de recriar a especificação DOM do JavaScript mostra que o TypeScript ainda é um subconjunto de Javascript NÃO um superconjunto .

No que diz respeito à implementação desse recurso, concordo com @gmurray81 e muitos outros que argumentaram que o tipo do getter deve ser um subconjunto do tipo de união de um setter. Isso garante que não haja digitação desarticulada. Essa abordagem permite que a função de um setter limpe a entrada sem destruir o tipo do getter (ou seja, ser forçado a recorrer ao uso de any ).

Aqui está o que eu não posso fazer. Algo simples que já fiz parecido no JS mas não consigo no TS.

4yo problema que REALMENTE poderia ser implementado.

export const enum Conns {
  none = 0, d = 1, u = 2, ud = 3,
  r = 4, rd = 5, ru = 6, rud = 7,
  l = 8, ld = 9, lu = 10, lud = 11,
  lr = 12, lrd = 13, lru = 14, lrud = 15,
  total = 16
}

class  Tile {
  public connections = Conns.none;

  get connLeft() { return this.connections & Conns.l; };

  set connLeft(val: boolean) { this.connections = val ? (this.connections | Conns.l) : (this.connections & ~Conns.l); }
}

Correndo para este problema agora. O abaixo parece uma solução sensata, para citar @calebjclark :

No que diz respeito à implementação desse recurso, concordo com @gmurray81 e muitos outros que argumentaram que o tipo do getter deve ser um subconjunto do tipo de união de um setter. Isso garante que não haja digitação desarticulada. Essa abordagem permite que a função de um setter limpe a entrada sem destruir o tipo do getter (ou seja, ser forçado a recorrer a qualquer um).

Para o getter ser um subconjunto do tipo do setter, acho que isso é desnecessariamente limitado. Realmente, o tipo de um setter _poderia_ teoricamente ser literalmente qualquer coisa, desde que a implementação sempre retorne um subtipo do tipo do getter. Exigir que o setter seja um supertipo do tipo do getter é uma limitação estranha que pode atender aos dois casos de uso apresentados neste ticket, mas certamente receberia reclamações mais tarde.

Dito isto, classes são implicitamente também interfaces, com getters e setters sendo essencialmente detalhes de implementação que não apareceriam em uma interface. No exemplo do OP, você terminaria com type MyClass = { myDate(): moment.Moment; } . Você teria que expor de alguma forma esses novos tipos getter e setter como parte da interface, embora eu pessoalmente não veja por que isso seria desejável.

Esse problema está basicamente pedindo uma versão menos limitada de sobrecarga de operador de = e === . (Getters e setters já estão sobrecarregando operadores.) E como alguém que já lidou com sobrecargas de operadores em outras linguagens, direi que sinto que elas devem ser evitadas na maioria dos casos. Claro, alguém poderia sugerir algum exemplo matemático como pontos cartesianos e polares, mas na grande maioria dos casos em que você usaria TS (incluindo os exemplos neste tópico), eu argumentaria que a necessidade de sobrecarga de operadores é provavelmente um cheiro de código. Pode parecer que está tornando a API mais simples, mas tende a fazer o oposto. Como mencionado anteriormente, ter o seguinte não necessariamente verdadeiro é super confuso e pouco intuitivo.

foo.x = y;
if (foo.x === y) { // this could be false?

(Acho que as únicas vezes em que vi setters usados ​​que pareciam razoáveis ​​são para registrar e definir um sinalizador "sujo" em objetos para que outra coisa possa dizer se um campo foi alterado. Nenhum deles realmente altera o contrato do objeto.)

@MikeyBurkman

mas na grande maioria dos casos em que você usaria TS (incluindo os exemplos neste tópico), eu diria que a necessidade de sobrecarga de operador é provavelmente um cheiro de código [ ... ]

 foo.x = y; 
 if (foo.x === y) { // this could be false?

Portanto, apenas ter os tipos correspondentes não impede um comportamento surpreendente e, de fato, el.style.display = ''; //so is this now true: el.style.display === ''? mostra que isso também não é teórico. Mesmo com campos antigos simples, a suposição não vale para NaN, a propósito.

Mas, mais importante, seu argumento ignora o ponto de que o TS não pode realmente opinar sobre essas coisas porque o TS precisa interoperar com as APIs JS existentes, incluindo grandes e improváveis ​​de mudar coisas como o DOM. E como tal, não importa se a API é ou não ideal; o que importa é que o TS não pode interoperar de forma limpa com tais apis. Agora, você é forçado a reimplementar qualquer lógica de fallback que a API usou internamente para coagir um valor fora do tipo getter passado para o setter e, assim, digitar a propriedade como o tipo getters, ou ignorar a verificação de tipo para adicionar tipo asserções inúteis em cada site de getters e, portanto, digite a propriedade como o tipo de setters. Ou, pior: faça o que o TS escolher anotar para o DOM, independentemente de isso ser o ideal.

Todas essas escolhas são ruins. A única maneira de evitar isso é aceitar a necessidade de representar as apis JS o mais fielmente possível, e aqui: isso parece possível (reconhecidamente sem nenhum conhecimento dos componentes internos do TS que podem tornar isso decididamente não trivial).

isso parece possível (reconhecidamente sem nenhum conhecimento dos componentes internos do TS que podem tornar isso decididamente não trivial)

Eu suspeito que algumas das novas sintaxes de declaração de tipo que eles habilitaram mais recentemente poderiam tornar isso exprimível quando era menos viável antes, então eles deveriam realmente considerar reabrir esse problema ... @RyanCavanaugh

Em um mundo ideal, o TS interoperaria com todas as APIs JS. Mas esse não é o caso. Existem vários idiomas JS que não podem ser digitados corretamente no TS. No entanto, eu prefiro que eles se concentrem em consertar coisas que realmente levam a linguagens de programação melhores, não piores. Embora apoiar isso de uma maneira sã seria bom, há uma dúzia de outras coisas nas quais eu pessoalmente prefiro que eles gastem seu tempo limitado primeiro. Isso está bem abaixo nessa lista.

@MikeyBurkman a questão da prioridade é válida. Minha principal preocupação é a decisão de @RyanCavanaugh de encerrar este problema e cancelá-lo. Ignorar todos os problemas apontados neste tópico conflita diretamente com a missão declarada do Typescript, que é ser um "superconjunto" de Javascript (em vez de um subconjunto).

Sim, posso definitivamente concordar que esse problema provavelmente não deve ser encerrado, pois é algo que _provavelmente_ deve ser corrigido eventualmente. (Embora eu realmente duvide que seja.)

Embora apoiar isso de uma maneira sã seria bom, há uma dúzia de outras coisas que eu pessoalmente prefiro que eles gastem seu tempo limitado primeiro

Acho que você está subestimando um aspecto importante do TypeScript, pois deveria torná-lo mais seguro usar as APIs DOM existentes e as APIs JS existentes. Ele existe para manter mais do que apenas seu próprio código, dentro de sua bolha, seguro.

Aqui está o meu ponto de vista, eu construo bibliotecas de componentes e, embora eu não necessariamente deliberadamente cause essa incompatibilidade entre setters/getters, devido aos meus druthers. Às vezes, minha mão é forçada com base em como minhas bibliotecas precisam interagir com outros sistemas no local. Por exemplo, Angular define todas as propriedades de entrada em um componente proveniente de marcação como strings, em vez de fazer qualquer tipo de coerção com base em seu conhecimento do tipo de destino (pelo menos, pela última vez que verifiquei). Então, você se recusa a aceitar strings? Você faz de tudo uma string, mesmo que isso torne seus tipos horríveis de usar? Ou você faz o que o TypeScript gostaria que você fizesse e usa um tipo como: string | Cor, mas faça com que o uso dos getters seja horrível. Estas são todas opções terríveis que reduzem a segurança, quando alguma expressividade extra do sistema de tipos teria ajudado.

O problema é que não é apenas o Angular que causa esses problemas. Angular acabou nessa situação porque espelha muitos cenários no DOM onde a autocoerção acontece em um conjunto de propriedades, mas o getter é sempre um tipo singular antecipado.

Olhe um pouco upthread: Angular é muito pior do que você pensa .

Para mim, geralmente é o problema quando você deseja criar um wrapper em torno de algum armazenamento de uso geral, que pode armazenar pares de valores-chave e deseja limitar os usuários a determinadas chaves.

class Store {
  private dict: Map<string, any>;

  get name(): string | null {
    return this.dict.get('name') as string | null;
  }

  set name(value: string) {
    this.dict.set('name', value);
  }
}

Você deseja ter essa restrição: o usuário pode obter null , se o valor não foi definido anteriormente, mas não pode defini-lo como null . Atualmente, você não pode fazer isso, porque o tipo de entrada do setter deve incluir null .

@fan-tom, ótimo caso de uso do motivo pelo qual esse problema precisa ser reaberto! Continue vindo.

Esta questão tem um número extremamente alto de votos positivos!

Este é o projeto da equipe TS, para que eles possam fazer o que bem entenderem. Mas se eles pretendem tornar isso o mais útil possível para uma comunidade externa de usuários, espero que a equipe TS possa levar em consideração o alto número de votos da comunidade.

A página inicial de typescriptlang.org é atualmente imprecisa quando afirma:

TypeScript é um superconjunto tipado de JavaScript que compila para JavaScript simples.

TypeScript é um _superconjunto_ tipado de um _subconjunto_ de JavaScript que compila para JavaScript simples. :risonho:

TypeScript é um superconjunto tipado de um subconjunto de JavaScript que compila para JavaScript simples.

Mais diplomaticamente, acho que você poderia dizer que é um superconjunto opinativo de JavaScript, quando a equipe faz esse tipo de omissão opinativa da modelagem do sistema de tipos.

Eles estão dizendo: "nós não vamos modelar isso, porque é difícil para nós fazer, e achamos que você não deveria fazer isso, de qualquer maneira". Mas o JavaScript está repleto de coisas que você _não deveria_ fazer, e geralmente o Typescript não o impedirá de fazer essas coisas se você tiver vontade e know-how, porque parece que a estratégia geral é não ser opinativo sobre o que JavaScript você pode e não pode simplesmente executar como Typescript.

É por isso que é tão estranho se recusar a modelar esse cenário comum (usado no DOM!), citando a crença de que as APIs não devem executar coerção baseada em setter como a justificativa para não fazê-lo.

Atualmente, isso não parece ser possível, e eu tenho que recorrer a algo assim:

class MyClass {

    private _myDate: moment.Moment;

    get myDate(): moment.Moment {
        return this._myDate;
    }

    set myDate(value: moment.Moment) {
        assert.fail('Setter for myDate is not available. Please use: setMyDate() instead');
    }

    setMyDate(value: Date | moment.Moment) {
        this._myDate = moment(value);
    }
}

O melhor que você pode fazer é adicionar um construtor e chamar a função setter personalizada lá, se essa for a única vez que você estiver configurando esse valor.

constructor(value: Date | moment.Moment) {
    this.setMyDate(value);
}

O problema ao atribuir valores diretamente ainda permanece.

Alguma atualização sobre isso, depois de mais de 5,5 anos?

@xhliu como os rótulos indicam, é uma limitação de design muito complexa para resolver e, portanto, o problema está encerrado. Eu não esperaria uma atualização. O período de tempo para uma questão encerrada é meio irrelevante.

Por favor, reabra. Também isso precisa ser adicionado no nível da interface. Um exemplo clássico é ao implementar um Proxy onde você tem total flexibilidade sobre o que pode ler e escrever.

@xhliu ... muito complexo para resolver...

https://ts-ast-viewer.com/#code/MYewdgzgLgBFCm0DyAjAVjAvDA3gKBkJgDMQQAuGAIhQEMAnKvAXzzwWXQDpSQg

const testObj = {
    foo: "bar"
}

testObj.foo

O símbolo foo tem isso a dizer:

  foo
    flags: 4
    escapedName:"foo"
    declarations: [
      PropertyAssignment (foo)
    ]
    valueDeclaration: PropertyAssignment (foo)

Há muito espaço aqui para informações adicionais, o que é um ótimo design para o TS. Quem projetou isso provavelmente o fez especificamente para tornar possíveis recursos adicionais (mesmo grandes) no futuro.

Especulativo a partir deste ponto:

Se fosse possível declarar (por exemplo de pseudocódigo) um accessDelclaration: AccessSpecification (foo) , então o PropertyAccessExpression que conhece o símbolo foo e suas declarações, poderia verificar condicionalmente se existe um "accessDelclaration" e use a digitação disso em vez disso.

Supondo que a sintaxe exista para adicionar essa propriedade accessDelclaration ao símbolo "foo", a PropertyAccessExpression deve ser capaz de extrair a "AccessSpecification" do símbolo que obtém de ts.createIdentifier("foo") e produzir diferentes tipos.

ts.createPropertyAccess(
  ts.createIdentifier("testObj"),
  ts.createIdentifier("foo")
)

Especulativamente, parece que a parte mais difícil desse desafio provavelmente seria a quantidade de perda de velocidade em torno da sintaxe (ou talvez uma filosofia da empresa?), mas do ponto de vista da implementação, todas as ferramentas deveriam estar lá. Uma única condição seria adicionada à função ts.createPropertyAccess() , e uma classe Declaration para representar essa condição e seus efeitos deve ser adicionada ao símbolo da propriedade do objeto.

Muitos bons exemplos foram escritos por que isso é desesperadamente necessário (especialmente para DOM e Angular).

Vou apenas acrescentar que fui atingido por isso hoje ao migrar o código JS antigo para o TS, onde atribuir string a window.location não funcionou e tive que fazer as any solução alternativa 😟

A propriedade somente leitura Window.location retorna um objeto Location com informações sobre a localização atual do documento.

Embora Window.location seja um objeto Location somente leitura, você também pode atribuir uma DOMString a ele. Isso significa que você pode trabalhar com localização como se fosse uma string na maioria dos casos: location = ' http://www.example.com ' é sinônimo de location.href = ' http://www.example.com ' .
fonte

migrando o código JS antigo para o TS, onde atribuir string a window.location não funcionou e eu tive que fazer as any solução alternativa

Esse é um ótimo exemplo.

TS precisa disso. Esta é uma parte muito normal do JavaScript.

Vejo que o problema atual foi encerrado. Mas pelo que entendi, essa funcionalidade não foi percebida?

@AGluk , esse problema precisa ser reaberto.

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

Questões relacionadas

wmaurer picture wmaurer  ·  3Comentários

weswigham picture weswigham  ·  3Comentários

Roam-Cooper picture Roam-Cooper  ·  3Comentários

manekinekko picture manekinekko  ·  3Comentários

Antony-Jones picture Antony-Jones  ·  3Comentários