Typescript: Sugerencia: permita que los accesores get/set sean de diferentes tipos

Creado en 26 mar. 2015  ·  125Comentarios  ·  Fuente: microsoft/TypeScript

Sería genial si hubiera una manera de relajar la restricción actual de requerir que los descriptores de acceso get/set tengan el mismo tipo. esto sería útil en una situación 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);
    }
}

Actualmente, esto no parece ser posible, y tengo que recurrir a algo como esto:

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

Esto está lejos de ser ideal, y el código sería mucho más limpio si se permitieran diferentes tipos.

¡Gracias!

Design Limitation Suggestion Too Complex

Comentario más útil

Un getter y setter de JavaScript con diferentes tipos es perfectamente válido y funciona muy bien y creo que es la principal ventaja/propósito de esta función. Tener que proporcionar un setMyDate() solo para complacer a TypeScript lo arruina.

Piense también en las bibliotecas JS puras que seguirán este patrón: los .d.ts tendrán que exponer una unión o any .

El problema es que los accesores no aparecen en .d.ts de manera diferente a las propiedades normales.

Luego, esta limitación debe solucionarse y este problema debe permanecer abierto:

// 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 comentarios

Puedo ver cómo esto sería bueno aquí (y esto se ha solicitado antes, aunque no puedo encontrar el problema ahora), pero no es posible si la utilidad es suficiente o no para garantizarlo. El problema es que los accesores no aparecen en .d.ts de manera diferente a las propiedades normales, ya que parecen iguales desde esa perspectiva. Lo que significa que no hay diferenciación entre getter y setter, por lo que no hay forma de a) requerir que una implementación use un elemento de acceso en lugar de un miembro de una sola instancia yb) especificar la diferencia en los tipos entre getter y setter.

Gracias por la rápida respuesta, Dan. Seguiré el camino menos elegante. Gracias por el gran trabajo!

Un getter y setter de JavaScript con diferentes tipos es perfectamente válido y funciona muy bien y creo que es la principal ventaja/propósito de esta función. Tener que proporcionar un setMyDate() solo para complacer a TypeScript lo arruina.

Piense también en las bibliotecas JS puras que seguirán este patrón: los .d.ts tendrán que exponer una unión o any .

El problema es que los accesores no aparecen en .d.ts de manera diferente a las propiedades normales.

Luego, esta limitación debe solucionarse y este problema debe permanecer abierto:

// 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)'
}

Me doy cuenta de que esto es solo una opinión, pero escribir un setter de modo que a.x === y no sea true inmediatamente después a.x = y; es una bandera roja gigante. ¿Cómo saben los consumidores de una biblioteca como esa qué propiedades tienen efectos secundarios mágicos y cuáles no?

¿Cómo [usted] sabe qué propiedades tienen efectos secundarios mágicos y cuáles no?

En JavaScript puede ser poco intuitivo, en TypeScript las herramientas (IDE + compilador) se quejarán. ¿Por qué nos encanta TypeScript de nuevo? :)

Un getter y setter de JavaScript con diferentes tipos es perfectamente válido y funciona muy bien y creo que es la principal ventaja/propósito de esta función.

Esto argumenta que JavaScript tiene un tipo débil, por lo que TypeScript debería tener un tipo débil. :-S

Esto argumenta que JavaScript está escrito débilmente, por lo que TypeScript debe escribirse débilmente

C# lo permite y eso no hace que este lenguaje esté tipificado débilmente . C# no permite obtener/establecer tipos diferentes.

C# lo permite y eso no hace que este lenguaje esté tipificado débilmente . C# no permite obtener/establecer tipos diferentes.

:guiño:

Nadie está discutiendo (por lo que puedo ver) que los accesores deben escribirse débilmente, están discutiendo que deberíamos tener la flexibilidad para definir el tipo (s).

A menudo, es necesario copiar un objeto simple y antiguo en instancias de objetos.

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

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

Eso es mucho mejor que la alternativa y permite que mi setter encapsule el tipo exacto de lógico para el que están hechos los setters.

@paulwalker , el patrón que usa allí (un setter que toma any y un getter que devuelve un tipo más específico) es válido hoy.

@danquirk ¡Es genial saberlo, gracias! Parece que solo necesitaba actualizar mi compilador de complementos IDE para ST.

@danquirk Eso no parece funcionar de acuerdo con el patio de recreo (o la versión 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%20return%20%5B%5D%3B%0A%20%20%7D%0A%20%20%0A%20%20set%20items(value%3A%20any)%20%7B%0A% 09%20%20%0A%20%20%7D%0A%7D

Acabo de probar con typescript@next (Versión 1.8.0-dev.20151102), y también tengo un error.

~$ 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.

Irónicamente, después de actualizar mi Sublime linter, ya no arrojó un error, que usa TypeScript 1.7.x. He estado asumiendo que es una próxima mejora en 1.7+, por lo que quizás 1.8.0 retrocedió.

Incluso con la versión del código de Visual Studio (0.10.5 (diciembre de 2015)) que admite TypeScript 1.7.5 y con TypeScript 1.7.5 instalado globalmente en mi máquina, esto sigue siendo un problema:

image

Entonces, ¿qué versión será compatible?
Gracias

Creo que Dan estaba equivocado. El getter y el setter deben ser del mismo tipo.

lástima. Hubiera sido una buena característica para escribir objetos de página para usar en pruebas de transportador.

Habría podido escribir una prueba de transportador:

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

... al crear un 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);
    }
}

¿Qué pasa con ese código que requiere que el setter sea del tipo any ?

El getter devolverá un webdriver.promise.Promise<string> pero el setter quiero asignarle un string .

Tal vez la siguiente forma más larga de la prueba del transportador lo haga más claro:

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

@RyanCavanaugh Con la introducción de anotaciones nulas, esto evita que el código permita llamar a un setter con nulo para establecerlo en algún valor predeterminado.

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

¿Podría considerar al menos permitir que los tipos difieran en presencia de | null y | undefined ?

Esto realmente sería una buena característica.

Entonces, ¿será esta una característica?

@artyil está cerrado y etiquetado como _Por diseño_ lo que indica que actualmente no hay planes para agregarlo. Si tiene un caso de uso convincente que cree que anula las preocupaciones expresadas anteriormente, no dude en presentar su caso y los comentarios adicionales pueden hacer que el equipo central reconsidere su posición.

@kitsonk Creo que se han proporcionado más que suficientes casos de uso convincentes en los comentarios anteriores. Si bien el diseño actual sigue un patrón común de la mayoría de los otros lenguajes escritos con esta restricción, es innecesaria y demasiado restrictiva en el contexto de Javascript. Si bien es por diseño, el diseño está mal.

Estoy de acuerdo.

Después de pensar en esto un poco más. Creo que el problema aquí es realmente la complejidad de la implementación. Personalmente, encuentro convincente el ejemplo de @Arnavion , pero el sistema de tipos actual trata a los getters/setters como propiedades regulares. para que esto funcione, ambos deben tener el mismo valor. admitir tipos de lectura/escritura sería un gran cambio, no estoy seguro de que la utilidad aquí valga la pena el costo de implementación.

Si bien me encanta TypeScript y aprecio todo el esfuerzo que el equipo pone en él (¡realmente, ustedes son geniales!), Debo admitir que estoy decepcionado con esta decisión. Sería una solución mucho mejor que las alternativas de getFoo()/setFoo() o get foo()/set foo()/setFooEx() .

Sólo una breve lista de problemas:

  • Actualmente asumimos que las propiedades tienen exactamente un tipo. Ahora necesitaríamos distinguir entre el tipo de "lectura" y el tipo de "escritura" de cada propiedad en cada ubicación
  • Todas las relaciones de tipo se vuelven sustancialmente más complejas porque tenemos que razonar sobre dos tipos por propiedad en lugar de uno (¿se puede { get foo(): string | number; set foo(): boolean } a { foo: boolean | string | number } o viceversa?)
  • Actualmente asumimos que una propiedad, después de establecerse, todavía tiene el tipo del valor establecido en la siguiente línea (lo que aparentemente es una suposición incorrecta en las bases de código de algunas personas, ¿qué?). Probablemente tendríamos que "desactivar" cualquier análisis de control de flujo en propiedades como esta

Honestamente, realmente trato de abstenerme de ser prescriptivo aquí sobre cómo escribir código, pero realmente me opongo a la idea de que este código

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

nunca debería imprimir algo que no sea "hello" en un lenguaje que intente tener una semántica sana. Las propiedades deben tener un comportamiento que sea indistinguible de los campos para los observadores externos.

@RyanCavanaugh , si bien estoy de acuerdo con usted en la opinión inyectada, puedo ver un contraargumento de que _podría_ ser muy TypeScripty ... Lanzar algo débilmente escrito a un setter, pero tener algo siempre fuertemente escrito devuelto, por ejemplo:

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

Aunque personalmente tiendo a pensar que si vas a ser tan mágico, lo mejor es crear un método para que el desarrollador final pueda entender claramente qué se doblará, plegará y mutilará.

Este es nuestro caso de uso para esta función. Nuestra API introdujo una capacidad que llamamos _Autocasting_. El principal beneficio es una experiencia de desarrollador simplificada que puede eliminar la cantidad de clases para importar para asignar propiedades para las cuales el tipo está bien definido.

Por ejemplo, una propiedad de color se puede expresar como una instancia Color o como una cadena CSS como rgba(r, g, b, a) o como una matriz de 3 o 4 números. La propiedad todavía se escribe como instancia de Color , ya que es el tipo de lo que obtiene al leer el valor.

Alguna información al respecto: https://developers.arcgis.com/javascript/latest/guide/autocasting/index.html

Nuestros usuarios han estado muy contentos de obtener esa función, reduciendo la cantidad de importaciones necesarias, y entienden perfectamente que el tipo cambia la línea después de la asignación.

Otro ejemplo 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)

Entonces, el setter es string | null | undefined y el getter es solo string . ¿Qué tipo deberíamos usar en las definiciones de tipo para esta biblioteca? Si usamos el primero, el compilador requerirá verificaciones nulas inútiles en todas partes. Si usamos el último, no podremos asignar null a esta propiedad.

Tengo otro ejemplo en el que me gustaría que el getter devuelva anulable, pero donde el setter nunca debe permitir nulo 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!

Entiendo que podría ser demasiado trabajo construir una lógica especial para permitir que los getters/setters difieran en nulo, y no es un gran problema para mí, pero sería bueno tenerlo.

@Elephant-Vessel filosóficamente, me encanta el ejemplo, representa bien la naturaleza voluble de los seres humanos, pero no estoy convencido de que no lo represente aún mejor al permitir null (o undefined ) para ajustar. ¿Cómo puedo modelar la falla de sinapsis en el sistema?

@aluanhaddad ¿Por qué querrías una falla de sinapsis? No quiero ese tipo de cosas malas en mi universo;)

¿Alguna actualización sobre esto? ¿Qué pasa con tener un valor predeterminado cuando se establece en nulo o indefinido?

No quiero que el consumidor tenga que anular la verificación de antemano. Actualmente, tengo que hacer que el setter sea un método separado, pero me gustaría que fueran iguales.

A continuación se muestra lo que me gustaría tener:

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

Me parece que tener el beneficio de la verificación no nula conlleva problemas, y este es uno de ellos. Con la verificación estricta de valores nulos desactivada, esto es posible, pero no obtiene la ayuda del compilador para evitar excepciones de referencias nulas. Sin embargo, si desea asistencia del compilador, creo que debería incluir más soporte, como tener definiciones separadas para getters y setters con respecto al menos a la nulabilidad, si nada más.

Las etiquetas en el problema indican que es una limitación de diseño y la implementación se consideraría demasiado compleja, lo que esencialmente significa que si alguien tiene una razón súper convincente por la que debería ser así, no va a ninguna parte.

@mhegazy @kitsonk Puedo ser parcial, pero siento que este es un error que apareció para la verificación estricta de valores nulos en un patrón común, especialmente en otros lenguajes como llaves en los que aún no tienen verificación de valores nulos. Una solución alternativa requeriría que el consumidor use el operador bang o verifique que en realidad nunca sea nulo (que es el punto de nunca ser nulo con el uso de valores predeterminados).

Esto se descompone una vez que agrega una verificación nula estricta porque ahora son tipos técnicamente diferentes. No estoy pidiendo que se configuren tipos diferentes fuertes, pero parece que los requisitos de diseño para habilitar esto también permitirían tipos diferentes fuertes también.

Se podría usar un diseño alternativo para los tipos débiles, de modo que los tipos como nulo e indefinido tendrían un caso especial para las definiciones de interfaz y los archivos d.ts si no desean habilitar tipos completamente diferentes.

En respuesta a https://github.com/Microsoft/TypeScript/issues/2521#issuecomment -199650959
Aquí hay un diseño de propuesta que debería ser menos complejo 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 hay algunas personas que miran este hilo que están mucho más familiarizadas con TypeScript, por lo que tal vez alguien que aún esté prestando atención pueda responder una pregunta relacionada. En este problema de Cesium, mencioné la limitación de tipo get/set que estamos discutiendo aquí, y la gente de Cesium dijo que el patrón que siguen proviene de C#: es el constructor implícito .

¿Puede TypeScript admitir constructores implícitos? Es decir, ¿puedo decir que myThing.foo siempre devuelve un Bar y se le puede asignar un Bar directamente, pero también se le puede asignar un number que ser envuelto / usado silenciosamente para inicializar un Bar , como una conveniencia para el desarrollador? Si es posible hacer esto anotando Bar , o tal vez diciendo específicamente que " number es asignable a Bar<number> ", abordaría el caso de uso discutido en el problema de Cesium, y también muchas de las cuestiones planteadas en este hilo.

Si no, ¿debo sugerir soporte de constructor implícito en un problema separado?

Cuanto más pienso/leo al respecto, más seguro estoy de que el "patrón de constructor implícito" necesitará la función descrita en este problema. La única forma en que es posible en Vanilla JS es usar objetos de acceso get/set, porque esa es la única vez que la asignación nominal (operador = ) está realmente llamando a una función definida por el usuario. (¿Verdad?) Entonces, creo que realmente necesitaremos

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

lo que parece que @RyanCavanaugh piensa que no es "semántica sana".

El simple hecho es que existe una biblioteca JS bastante popular que usa este patrón, y parece que es difícil, si no imposible, de describir dadas las restricciones de TS existentes. Espero estar equivocado.

JavaScript ya permite el patrón que describe. El _desafío_ es que, por diseño, se supone que el lado de lectura y escritura de los tipos en TypeScript son iguales. Hubo una modificación en el lenguaje para no permitir la asignación ( readonly ), pero hay algunos problemas que han solicitado el concepto de _solo escritura_, que se han discutido como demasiado complejos para tener poco valor en el mundo real.

En mi opinión, desde que JavaScript permitió accesos, las personas potencialmente crearon API confusas con ellos. Personalmente, me resulta confuso que algo en la asignación cambie _mágicamente_ a otra cosa. Cualquier cosa implícita, especialmente la conversión de tipo, es la ruina de JavaScript IMO. Es exactamente la flexibilidad lo que causa problemas. Con ese tipo de conversiones, donde hay _magia_, personalmente me gusta ver que se llame a los métodos, donde es un poco más explícito para el consumidor que algún tipo de ✨ ocurrirá y hará que un captador de solo lectura recupere valores.

Eso no significa que el uso en el mundo real no exista, eso es potencialmente sensato y racional. Supongo que se trata de la complejidad de dividir todo el sistema de tipos en dos, donde los tipos deben rastrearse en sus condiciones de lectura y escritura. Eso parece un escenario muy no trivial.

Estoy de acuerdo con los :sparkles: aquí, pero no estoy argumentando a favor o en contra del uso del patrón, solo estoy tratando de encontrar el código que ya lo usa y describir su forma. Ya funciona como funciona, y TS no me está dando las herramientas para describirlo.

Para bien o para mal, JS nos ha dado la capacidad de convertir asignaciones en llamadas a funciones y la gente lo está usando. Sin la capacidad de asignar diferentes tipos a los pares set/get , ¿por qué tener set/get en tipos ambientales? Salsa no tiene que saber que una propiedad se implementa con un getter y un setter si siempre va a tratar myThing.foo como una variable miembro de un solo tipo, independientemente del lado de la asignación en el que se encuentre. (Obviamente, la compilación real de TypeScript es otra cosa completamente diferente).

@thw0rted

Parece que hay algunas personas que miran este hilo que están mucho más familiarizadas con TypeScript, por lo que tal vez alguien que aún esté prestando atención pueda responder una pregunta relacionada. En este problema de Cesium, mencioné la limitación de tipo get/set que estamos discutiendo aquí, y la gente de Cesium dijo que el patrón que siguen proviene de C#: es el constructor implícito.

Los operadores de conversión definidos por el usuario implícitos de C# realizan la generación de código estático en función de los tipos de valores. Los tipos de TypeScript se borran y no influyen en el comportamiento del tiempo de ejecución ( async / await edge cases para Promise polyfillers a pesar).

@kitsonk Tengo que estar en desacuerdo en general con respecto a las propiedades. Habiendo pasado una buena cantidad de tiempo con Java, C++ y C#, me encantan las propiedades (como se ve en C#) porque brindan una abstracción sintáctica crítica (esto es algo cierto en JavaScript). Permiten que las interfaces segreguen las capacidades de lectura/escritura de manera significativa sin cambiar la sintaxis. Odio ver verbos desperdiciados en operaciones triviales como getX() cuando obtener X puede ser implícito.
En mi opinión, las API confusas, muchas de las cuales hacen lo que dices abusan de los accesores, se derivan más de demasiados _setters_ que hacen cosas mágicas.

Si tengo una vista de solo lectura, pero en vivo sobre algunos datos, digamos registry , creo que las propiedades de solo lectura son muy fáciles 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: '再见'}]

Perdón por la tangente, pero me encantan los accesorios para esto y creo que son fáciles de entender, pero estoy dispuesto a que me convenzan de lo contrario y la legibilidad es mi primera preocupación.

Cuando TypeScript limita JavaScript, se convierte más en una molestia que en una ventaja. ¿No está TypeScript destinado a ayudar a los desarrolladores a comunicarse entre sí?

Además, los setters se llaman mutadores por una razón. Si no necesitara ningún tipo de conversión, no usaría un setter, configuraría la variable yo mismo.

portando el proyecto javascript a mecanografiado. me encontré con este problema ..

Sería bueno usarlo en decoradores angulares @Input . Debido a que el valor se pasa desde una plantilla, sería mucho más limpio, en mi opinión, tratar con diferentes tipos de objetos entrantes.

Actualización: esto parece funcionar para mí

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

Entonces, ¿dónde estamos? Realmente me gustaría tener la posibilidad de devolver un tipo diferente con el getter que con el setter. Por ejemplo:

class Field {
  private _value: string;

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

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

Esto es lo que hace el 99% de las implementaciones nativas (si pasa un número a (input as HTMLInputElement).value , siempre devolverá una cadena. De hecho, get y set deben considerarse métodos y deben permitir algunos:

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 , cuando dice que su código "funciona", ¿ ErrorNotificationComponent.errors devuelve un tipo de Array<string> | any ? Eso significa que necesita guardias de tipo cada vez que lo usa, cuando sabe que solo puede devolver Array<string> .

@ lifaon74 , que yo sepa, no hay movimiento sobre este tema. Creo que se ha hecho un caso convincente: se presentaron múltiples escenarios, toneladas de código JS heredado que no se puede describir correctamente en Typescript debido a esto, pero el problema está cerrado. ¿Quizás si cree que los hechos sobre el terreno han cambiado, abra uno nuevo? No conozco la política del equipo sobre la reutilización de viejos argumentos, pero te respaldaría.

No conozco la política del equipo sobre la reutilización de viejos argumentos, pero te respaldaría.

Reconsiderarán temas previamente cerrados. Proporcionar un 👍 en la parte superior del problema da crédito a que es significativo. Creo que generalmente ha caído en la categoría "demasiado complejo" porque significaría que el sistema de tipos tendría que bifurcarse en cada lectura y escritura y sospecho que la sensación es el esfuerzo y el costo de poner eso para saciar lo que es un caso de uso válido pero poco común no vale la pena.

Mi sensación personal es que sería bueno tenerlo, especialmente por poder modelar el código JavaScript existente que usa este patrón de manera efectiva.

Personalmente, consideraría el asunto resuelto si se nos ocurriera alguna solución no totalmente onerosa para el problema de JS heredado. Todavía soy una especie de novato de TS, así que tal vez mi solución actual (fundición forzada o protectores de tipos innecesarios) no es un uso óptimo de las capacidades existentes.

Estar de acuerdo. El setter estará muy limitado cuando el tipo tenga que ser el mismo... Mejore.

Buscando recomendaciones para lograr esto:

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

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

Quiero que el valor almacenado sea un número, pero quiero convertirlo de cadena a flotante en el setter para no tener que analizar Float cada vez que configuro una propiedad.

Creo que el veredicto es que este es un patrón Javascript válido (o al menos razonablemente bien aceptado), que no encaja bien con las partes internas de Typescript. Tal vez si cambiar las partes internas es un cambio demasiado grande, ¿podríamos obtener algún tipo de anotación que cambie la forma en que el 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;

En otras palabras, tal vez podríamos marcar el TS de tal manera que el preprocesador (?) convierta todas las lecturas de w.price en una conversión explícita, antes de que el TS se transmita a JS. Por supuesto, no conozco el funcionamiento interno de cómo TS maneja las anotaciones, por lo que esto podría ser una basura total, pero la idea general es que tal vez sería más fácil convertir de alguna manera el TS en otro TS, que cambiar la forma en que el transpilador TS genera JS.

No digo que esta función no sea necesaria, pero aquí hay una forma de evitar set . En mi opinión get siempre debe devolver el mismo tipo de variable, por lo que esto fue lo suficientemente bueno para mí.

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 el error de compilación en su código

@iamjoyce => incorrecto porque el compilador asume val: number en set price(val)

Sí, también estoy aquí porque el uso de componentes de Angular parece querer que tengamos diferentes tipos de getter/setter.

Angular siempre inyectará una cadena si establece una propiedad de componentes (a través de un atributo) desde el marcado (a menos que use una expresión vinculante) independientemente del tipo de propiedad de entrada. Entonces, seguro que sería bueno poder modelar esto así:

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

Hoy en día, esto funciona si omitimos el | string pero tener eso describiría con mayor precisión cómo Angular termina usando el componente. En eso, si accede desde el código, el tipo de propiedad importa, pero si lo configura como un atributo, forzará una cadena.

No creo que en general queramos esta funcionalidad porque nos GUSTARÍA diseñar las API de esta manera. Estoy de acuerdo, que las propiedades son mejores si no tienen efectos secundarios de coerción debajo de las sábanas. Para evitar esto, sería genial si Angular se hubiera diseñado de tal manera que los conjuntos de atributos frente a los conjuntos de propiedades vinculantes entraran en diferentes puntos de entrada.

Pero si miramos esto pragmáticamente, es útil si TypeScript nos permite modelar las interacciones con bibliotecas externas que están usando nuestros tipos de tal manera que desafía el axioma de igualdad de tipos de lectura/escritura.

En este caso, sería muy problemático usar el tipo de unión en el getter, ya que requeriría todo tipo de molestos protectores/afirmaciones de tipo. Pero dejar el tipo de unión fuera del setter se siente mal, ya que cualquiera de las herramientas podría decidir en el futuro comenzar a intentar verificar que las propiedades que se establecen a partir de los atributos deben poder asignarse desde la cadena.

Entonces, en este caso, el sistema de tipos no es lo suficientemente expresivo para capturar cómo se permite que una biblioteca externa use el tipo. Esto no importa _inmediatamente_, ya que la biblioteca externa en realidad no consume estos tipos en el nivel de mecanografiado, con información de tipo. Pero eventualmente puede importar, ya que las herramientas pueden muy bien consumir la información del tipo.

Una persona mencionó anteriormente una solución alternativa que parece un poco desagradable, pero que probablemente podría funcionar, mediante la cual podríamos usar el tipo de unión tanto para el setter como para el getter, pero tener alguna forma de indicar que ciertos tipos de la unión son imposibles para el getter. De hecho, se eliminan previamente de la consideración en el flujo de control del sitio del sitio de llamada de getter como si alguien hubiera usado un tipo de protección para verificar que no están presentes. Sin embargo, eso parece molesto en comparación con permitir una unión de superconjunto en el setter en comparación con una unión de subconjunto en el getter.

Pero tal vez esa sea una forma de resolver las cosas internamente. Siempre que el setter sea solo una unión de superconjunto del tipo getter. Trate los tipos getter y setter como equivalentes internamente, pero marque partes del tipo de unión getter como imposibles. De modo que el análisis de flujo de control los elimina de la consideración. ¿Eso evitaría las restricciones de diseño?

Para profundizar en lo anterior. Tal vez sería útil, desde el punto de vista de la expresividad del tipo, poder indicar partes de un tipo compuesto como imposibles. Esto no afectaría la igualdad con otro tipo con la misma estructura pero sin los modificadores imposibles. Un modificador imposible solo afectaría el análisis del flujo de control.

Como sugerencia alternativa, si hubiera alguna sintaxis para aplicar una protección de tipo definida por el usuario a un valor de retorno, esto también sería suficiente. Me doy cuenta de que, en circunstancias normales, esto es un poco ridículo, pero tener ese tipo de expresividad en el valor de retorno, así como en los argumentos, ayudaría a resolver estos casos extremos.

La protección de tipo definida por el usuario en el valor de retorno, también tiene la ventaja de ser algo que podría expresarse en declaraciones/interfaces de tipo cuando se aplasta a una propiedad.

Solo agregando otro caso de uso aquí, mobx-state-tree permite establecer una propiedad de un par de maneras diferentes (desde instancias y tipos de instantáneas), sin embargo, solo la devolverá de una manera estándar única (instancias) desde el accesor get, por lo que esto sería extremadamente útil si fuera compatible.

Me estoy poniendo alrededor de esto así:

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

No creo que eso funcione en torno al problema en cuestión. Todavía necesitaría protectores de tipos cuando use el captador, aunque, en realidad, el captador solo devuelve un subconjunto del tipo de unión.

Estoy usando any para solucionar TSLint. Y el compilador tampoco se queja.

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 así pierdes seguridad 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 Es exactamente por eso que queremos que los accesores tengan diferentes tipos. Lo que encontré es una solución alternativa, no una solución.

@jpidelatorre mi solución actual es usar otra función como setter

export class FooBar {
  private bar: string;

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

  setFoo (value: Date | string ) {}
}

@keenondrums No es tan atractivo como los accesorios, pero es la mejor opción hasta ahora.

No es realmente un modelo de lo que sucede con las propiedades @input en un componente Angular, a menos que use una función separada para un getter.

Lo cual es demasiado feo.

También me gustaría esta función, pero necesito que funcione bien en archivos .d.ts, donde no hay soluciones alternativas. Estoy tratando de documentar algunas clases expuestas a través de Mocha (el puente Objective-C/Javascript), y las propiedades de instancia de los elementos envueltos se configuran así:

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

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

También he visto muchos casos en los que una API te permite establecer una propiedad con un objeto que coincide con una interfaz, pero el captador siempre devuelve una instancia de clase real:

interface IFoo {
    bar: string;
}

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

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

Me había olvidado en gran medida de este problema, pero se me ocurrió una idea mientras lo leía. Creo que nos hemos decidido por la idea de que vale la pena hacerlo en abstracto, pero es demasiado complicado desde el punto de vista técnico para ser factible. Ese es un cálculo de compensación: no es técnicamente imposible , simplemente no vale la pena el tiempo y el esfuerzo (y la complejidad del código adicional) para implementar. ¿Derecha?

¿El hecho de que esto haga que sea imposible describir con precisión la API DOM central cambia las matemáticas en absoluto? @RyanCavanaugh dice

Realmente me opongo a la idea de que este código foo.bar = "hello"; console.log(foo.bar); debería imprimir algo que no sea "hola" en un lenguaje que intente tener una semántica sensata.

Podríamos discutir si debería usarse de esta manera, pero el DOM siempre ha admitido construcciones como el.hidden=1; console.log(el.hidden) // <-- true, not 1 . Este no es solo un patrón que usan algunas personas, no es solo que esté en una biblioteca popular, por lo que podría ser una buena idea apoyarlo. Es un principio básico de cómo JS siempre ha funcionado, un poco de DWIM integrado en el alma del lenguaje, y hacerlo imposible a nivel de lenguaje rompe el principio fundamental de TS, que debe ser un "superconjunto" de JS. . Es una burbuja fea que sobresale del diagrama de Venn y no debemos olvidarla.

Es por eso que todavía me gusta la idea de mantener el setter/getter con el mismo tipo:

number | boolean

Pero introduciendo algún tipo de typegaurd donde puede indicar que, mientras que el getter técnicamente tiene el mismo tipo de unión, en realidad solo devolverá un subconjunto de los tipos en la unión. ¿Tratarlo como una especie de protección reductora del captador (¿una cuestión de flujo de inferencia?) no lo hace más sencillo que una modificación del modelo de tipos? (Él dice que no sabe nada sobre las partes internas...)

Esto podría, alternativamente, estar implícito si el tipo usado en el getter fuera un subconjunto estricto del tipo setters.

Esto podría, alternativamente, estar implícito si el tipo usado en el getter fuera un subconjunto estricto del tipo setters.

👍!

Creo que todos podemos estar de acuerdo en que no debería poder obtener un tipo que no sea configurable; Me gustaría alguna forma de reducir automáticamente el tipo en get para que sea el tipo que sé que siempre será.

No entiendo por qué algunos objetan que violaría la seguridad de tipo y todo eso. No veo que viole ningún tipo de seguridad si permitimos que al menos el setter tenga un tipo diferente, porque de todos modos tenemos un control en el lugar que no permitirá que otro tipo se establezca en una propiedad.
Ex:
Digamos que tengo una propiedad como Arraypero desde DB esto se devolverá como una cadena con separación de comas como, por ejemplo, '10,20,40'. Pero no puedo asignar eso a la propiedad de modo ahora, por lo que sería muy útil si pudiera permitir algo como

privado _employeeIdList: número []

obtener EmployeeIDList(): número[] {
devolver esto._employeeIdList;
}
establecer EmployeeIDList(_idList: cualquiera ) {
if (tipo de _idList== 'cadena') {
this._employeeIdList = _idList.split(',').map(d => Number(d));
}
else if (tipo de _idList== 'objeto') {
this._employeeIdList = _idList as number[];
}
}

Habría resuelto fácilmente este problema y es perfectamente seguro, aunque permite un tipo diferente en SET, pero aún así nos impide asignar un tipo incorrecto a la propiedad. Así que ganar ganar. Espero que los miembros del equipo renuncien a su ego y traten de pensar en el problema que crea y lo arreglen.

Me gustaría comentar que sigo pensando que esto es realmente importante para modelar el comportamiento de las bibliotecas JS existentes.

Si bien está muy bien levantar la nariz y definir un setter que fuerza un tipo a un subconjunto del tipo entrante y siempre devuelve ese subconjunto en el getter como un olor de código, en realidad, este es un patrón razonablemente común en JS land. .

Creo que TypeScript es encantador porque nos permite ser muy expresivos al describir las cagaries de las API JS existentes, incluso si no tienen API estructuradas idealmente. Creo que este es un escenario en el que si TypeScript fuera lo suficientemente expresivo como para permitirnos indicar que el captador siempre devolvería un tipo que es el subconjunto estricto del tipo del captador, esto agregaría una enorme cantidad de valor en el modelado de las API existentes, incluso el DOM!

Trusted Types es una nueva propuesta de API de navegador para luchar contra DOM XSS. Ya está implementado en Chromium (detrás de una bandera). La mayor parte de la API modifica los configuradores de varias propiedades DOM para aceptar tipos de confianza. Por ejemplo, .innerHTML acepta TrustedHTML | string mientras que siempre devuelve string . Para describir la API en TypeScript, necesitaríamos solucionar este problema.

La diferencia con los comentarios anteriores es que esta es una API de navegador (y no una biblioteca de usuario) que no se puede cambiar fácilmente. Además, el impacto de cambiar el tipo de Element.innerHTML a any (que es la única solución posible actualmente) es mayor que describir de manera imprecisa una biblioteca de usuario.

¿Existe la posibilidad de que esta solicitud de extracción se vuelva a abrir? ¿O hay otras soluciones que me perdí?

CC: @mprobst , @koto.

En un lenguaje que admite unión de tipos como TypeScript, esta característica es natural y un punto de venta.

@RyanCavanaugh , incluso cuando getter y setter tienen el mismo tipo, no se garantiza que o.x === y después o.x = y , ya que el setter puede desinfectar antes de guardar el valor.

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

Secundo la preocupación de @vrana. El comportamiento actual hace que sea imposible modelar algunas de las API existentes en Typescript.

Esto es especialmente cierto para las API web, para muchas de las cuales los tipos setter y getter son diferentes. En la práctica, los configuradores de API web realizan todo tipo de coerción de tipos, algunos de ellos especificados directamente para una función determinada del navegador, pero la mayoría de ellos implícitamente a través de IDL . Muchos de los configuradores también mutan el valor, consulte, por ejemplo, la especificación de la interfaz de ubicación . Este no es un error de un solo desarrollador: es una especificación de la API contra la que codifican los desarrolladores web.

Restringir los tipos de getter permite que Typescript represente esas API, lo que ahora es imposible.

Puede representar esas API porque es correcto decir que el tipo de propiedad es la unión de los posibles tipos que puede proporcionar al setter u obtener del getter.

Simplemente no es _eficiente_ describir una API de esa manera. Estás requiriendo que el consumidor use una protección de tipos en cada caso en que usen el getter para reducir los tipos posibles.

Eso está bien si no ha hecho que sea un axioma que siempre devolverá un tipo reducido de un captador, ya que muchas API web incluso bloquean sus especificaciones.

Pero incluso dejando eso a un lado por un momento, y hablando de las API de usuario, un caso de uso sólido para aceptar tipos de unión en un setter es el "scripty". Queremos aceptar un rango de tipos discretos que podemos obligar aceptablemente al tipo que realmente queremos.

¿Por qué permitir eso? Facilidad de uso.

Es posible que eso no importe para las API que está desarrollando internamente para el uso de su propio equipo, pero puede ser muy importante para las API diseñadas para el consumo público y generalizado.

Es encantador que TypeScript pueda permitirnos describir con precisión una API que ha relajado los tipos aceptables para algunas de sus propiedades, pero ese beneficio se ve empañado por la fricción en el otro extremo donde se requiere una verificación/protección excesiva de tipos para determinar el tipo de retorno del captador. , que preferiríamos _especificar_.

Yo diría que es un escenario diferente al caso idealizado que plantea @RyanCavanaugh . Ese caso implica que getter y setter siempre deben tener el mismo tipo de unión porque su campo de respaldo también tiene el mismo tipo de unión, y siempre hará un viaje de ida y vuelta de ese valor, y cambiar su tipo no tiene sentido.

Creo que el caso se centra en un uso más idealizado de los tipos de unión, en el que se trata de tipos construidos y realmente trata ese tipo de unión como una unidad semántica, para la que probablemente debería haber creado un alias.

type urlRep = string | Url;

La mayoría de las cosas simplemente lo harán de ida y vuelta, y tratarán solo con accesorios comunes, y en algunos casos romperá la caja negra con algunos tipos de guardias.

Yo diría que es un escenario completamente diferente de lo que estamos describiendo aquí. Lo que estamos describiendo es la realidad de que las API de uso general/público, especialmente para su uso en lenguajes de secuencias de comandos como JavaScript, a menudo relajan deliberadamente los tipos aceptables para un setter, porque hay una variedad de tipos que aceptarán obligar al tipo ideal, por lo que lo ofrecen como una mejora de la calidad de vida para aceptar todo eso.

Este tipo de cosas pueden parecer absurdas si usted es tanto el productor como el consumidor de una API, ya que solo genera más trabajo, pero puede tener mucho sentido si está diseñando una API para el consumo masivo.

Y no creo que nadie quiera que el setter/getter tenga tipos _disjuntos_. Lo que se discute aquí es que el productor de la API afirma al consumidor de la API que el captador devolverá un valor con un tipo que es un subconjunto estricto del tipo de unión del establecedor.

Y no creo que nadie quiera que el setter/getter tenga tipos disjuntos.

Solo para proporcionar un punto de vista contrario a eso. Definitivamente me gustaría eso porque tener tipos disjuntos para setter/getter es completamente legal en javascript. Y creo que el objetivo principal debería ser hacer que el sistema de tipos sea lo suficientemente expresivo para escribir correctamente cualquier cosa que sea legal en javascript (al menos en el nivel .d.ts). Entiendo que no es una buena práctica, tampoco lo son los objetos globales, por ejemplo, o cambiar el prototipo de funciones integradas como funciones, etc. Sin embargo, todavía podemos escribirlos bien en archivos .d.ts y no he escuchado a nadie lamentarlo. podemos hacer eso (aunque hace que algunos archivos de definición de tipos mal diseñados aparezcan en tipos definidos).

Acabo de encontrarme con una situación en la que necesito que esto sea una función para tener los tipos correctos que no controlo. El setter necesita ser más liberal y obligar a un tipo más conservador que el getter pueda devolver consistentemente.

Realmente desearía que existiera esta característica. Estoy transfiriendo el código JavaScript a TypeScript, y es un fastidio tener que refactorizar los setters si quiero escribir con seguridad.

Por ejemplo, tengo algo como esto:

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
        }

        // ...
}

Y como puedes imaginar, el uso es muy flexible:

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

Este tiene 4 años. ¿Cómo es que TODAVÍA no está arreglado? ¡Esta característica, como se menciona en los comentarios anteriores, es significativa! ¿Al menos considerar reabrir el tema?

Y estoy completamente de acuerdo con @kitsonk :

@kitsonk Creo que se han proporcionado más que suficientes casos de uso convincentes en los comentarios anteriores. Si bien el diseño actual sigue un patrón común de la mayoría de los otros lenguajes escritos con esta restricción, es innecesaria y demasiado restrictiva en el contexto de Javascript. Si bien es por diseño, el diseño está mal.

TypeScript 3.6 introdujo la sintaxis para escribir descriptores de acceso en archivos de declaración , al tiempo que retuvo la restricción de que el tipo de getter y setter debe ser idéntico.

Nuestro conjunto de herramientas de interfaz de usuario se basa en gran medida en los setters de conversión automática, donde el setter acepta más tipos que el tipo singular devuelto por el getter. Por lo tanto, sería bueno que TS ofreciera una forma de hacer que este patrón fuera seguro para los tipos.

Parece que @RyanCavanaugh dio la refutación más concreta de esta característica hace 3 años . Me pregunto si los casos de uso de DOM informados recientemente, así como la disponibilidad de una nueva sintaxis de declaración, podrían permitir una nueva decisión.

Sí, tan pronto como vi esa nueva función, inmediatamente pensé también en esta solicitud de función. También lo quiero para los propósitos de nuestros marcos de interfaz de usuario. Este es un caso de uso real, chicos.

La TSConf 2019 se llevará a cabo el 11 de octubre , pensando que sería una gran retrospección en la sesión de preguntas y respuestas, si alguien tuviera la oportunidad 🤔

Puede que haya dicho esto antes en este largo hilo, pero creo que vale la pena reiterarlo.

En mi opinión, el sistema de tipo expresivo en TypeScript tiene dos propósitos discretos. El primero es permitirle escribir código nuevo más seguro. Y si ese fuera el único propósito, tal vez podría argumentar que podría evitar este caso de uso, ya que el código puede ser más seguro si rechazara este escenario (pero creo que aún podríamos tener una discusión sobre esto).

Sin embargo, el segundo propósito es capturar el comportamiento de las bibliotecas tal como son y es una práctica razonablemente común en el DOM y en las bibliotecas JS auto-coaccionar en el setter, pero devolver un tipo esperado en el getter. Permitirnos capturar esto en el sistema de tipos nos permite describir con mayor precisión los marcos existentes tal como son .

Por lo menos, creo que Design Limitation podría eliminarse aquí, no creo que pertenezca más.

Aparentemente, esta es la razón por la que según #33749 .style.display = ...something nullable... ya no se verifica el tipo; que se presenta como un problema de corrección de nulabilidad. Eso es ser un poco engañoso, ¿no? Es molesto tener que descubrir nuevos cambios importantes cuando están mal etiquetados como correcciones de errores (busqué problemas reales que esto podría haber causado). Personalmente, encuentro mucho menos sorprendente que null tenga un comportamiento especial "usar predeterminado", que la cadena vacía; y hasta typecipt 3.7 preferí usar nulo para capturar eso. En cualquier caso, sería bueno si las anotaciones de tipo intencionalmente incorrectas realizadas para solucionar esta limitación se etiquetaran claramente como tales, para ahorrar tiempo en la clasificación de problemas de actualización.

También estoy interesado en encontrar un camino a seguir para esto. ¿Qué pasaría si solo estuviera permitido en contextos ambientales? @RyanCavanaugh , ¿eso ayudaría a abordar sus preocupaciones sobre la complejidad?

Mi caso de uso para esto es que tengo una API donde un proxy devuelve una promesa, pero una operación de configuración no establece una promesa. No puedo describir esto en TypeScript en este momento.

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

Ya sea que corrija el tipo de funciones nativas o proxies en general, TypeScript parece ser de la postura de que ese tipo de características fueron un error.

Realmente deberían quitar los números 6 y 7 de esta lista (https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals#goals)

Mi problema

Me gustaría enviar un elemento a una matriz en el método set y obtener la matriz completa en el método get. Por ahora tendré que usar set myArray(...value: string[]) que funciona bien. Me he metido mucho en este problema... por favor considere eliminarlo. Una información o advertencia funcionaría bien para esto.

Ejemplo (lo que me gustaría hacer)

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

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

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

Ejemplo (lo que tengo que hacer)

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

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

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

Una solución que se me ocurrió cuando necesitaba esto fue establecer que la propiedad fuera union type . Esto se debe a que mi tiempo llega como una cadena de la API, pero debo configurarlo como un objeto Momento para mi interfaz de usuario.

mi ejemplo

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

Llegué un poco tarde a la fiesta, pero recientemente me encontré con este problema. Es un dilema interesante, así que jugué un poco con él.

Supongamos que esto es algo que queremos hacer, una clase simple que tiene una propiedad que acepta cadenas y números (para cualquier propósito), pero la propiedad en realidad se almacena como una cadena:

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

Dado que esto no es posible y todavía queremos una tipificación fuerte, necesitamos un código adicional. Ahora supongamos para el propósito de nuestro ejemplo que alguien haya creado una biblioteca de propiedades que se vea así:

/**
 * 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, podríamos implementar nuestra propiedad con tipo para nuestra clase de esta manera:

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

Y luego, usar esta clase tendría un tipo de captador y definidor 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);

Y si necesita usar los setters o getters de propiedades de manera más dinámica, puede usar la función proxify para envolverlo:

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

Descargo de responsabilidad: si alguien quiere tomar este código y ponerlo en una biblioteca o hacer lo que quiera con él, no dude en hacerlo. Soy demasiado perezoso.

¡Solo unas pocas notas adicionales y luego prometo que dejaré de enviar spam a esta larga lista de personas!

Si añadimos esto a la biblioteca imaginaria:

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

Obtenemos una implementación que está un poco más cerca de la clase original y, lo que es más importante, tiene acceso 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;
}

No es lo más bonito del mundo pero funciona. Tal vez alguien pueda refinar esto y hacer algo que sea realmente agradable.

En el improbable caso de que cualquier persona que lea este hilo no esté convencida de que esta característica es importante, te doy este truco increíblemente feo que Angular acaba de presentar :

Sería ideal cambiar el tipo de valor aquí, de booleano a booleano|'', para que coincida con el conjunto de valores que realmente acepta el setter. TypeScript requiere que tanto el getter como el setter tengan el mismo tipo, por lo que si el getter debe devolver un valor booleano, entonces el setter se atasca con el tipo más estrecho... Angular admite la verificación de un tipo más amplio y permisivo para @Input() que es declarado para el propio campo de entrada. Habilite esto agregando una propiedad estática con el prefijo ngAcceptInputType_ a la clase de componente:

````
botón de envío de clase {
privado _deshabilitado: booleano;

obtener deshabilitado (): booleano {
devolver esto._deshabilitado;
}

establecer deshabilitado (valor: booleano) {
this._disabled = (valor === '') || valor;
}

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

Por favor , arregla esto.

Sí, literalmente tenemos esta fealdad por todas partes para respaldar los patrones que Angular quiere usar. La restricción actual es demasiado obstinada.

Si TS quiere ser utilizado para modelar la amplitud completa de las bibliotecas en la web, entonces debe esforzarse por ser menos obstinado, o no podrá modelar todos los diseños.

En este punto, creo que la responsabilidad recae en los contribuyentes para comunicar por qué este número no se está reabriendo. ¿Hay algún miembro contribuyente que esté incluso fuertemente en contra de esto en este momento?

Estamos creando una biblioteca DOM para NodeJs que coincida con la especificación W3C, pero este problema de Typescript lo hace imposible. ¿Cualquier actualización?

Me sorprende que algunos dentro del equipo central de TypeScript estén en contra de una función que ES REQUERIDA para recrear una de las bibliotecas de JavaScript más utilizadas en el planeta: el DOM.

No hay otra manera de a) implementar muchas partes de la especificación DOM del W3C mientras b) usar el sistema de tipos de TypeScript (a menos que recurra al uso de any por todas partes, lo que anula todo el propósito de TypeScript).

La página de inicio de typescriptlang.org actualmente es inexacta cuando dice:

TypeScript es un superconjunto escrito de JavaScript que se compila en JavaScript simple.

La incapacidad de recrear la especificación DOM de JavaScript muestra que TypeScript sigue siendo un subconjunto de Javascript NO un superconjunto .

Con respecto a la implementación de esta función, estoy de acuerdo con @gmurray81 y muchos otros que han argumentado que el tipo de captador debe ser un subconjunto del tipo de unión de setters. Esto asegura que no haya tipeo inconexo. Este enfoque permite que la función de un setter limpie la entrada sin destruir el tipo del getter (es decir, verse obligado a recurrir al uso de any ).

Esto es lo que no puedo hacer. Algo simple que hice similar en JS pero no puedo en TS.

Problema de 4 años que REALMENTE podría implementarse.

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

Me encuentro con este problema ahora. La siguiente parece una solución sensata, para citar a @calebjclark :

Con respecto a la implementación de esta función, estoy de acuerdo con @gmurray81 y muchos otros que han argumentado que el tipo de captador debe ser un subconjunto del tipo de unión de setters. Esto asegura que no haya tipeo inconexo. Este enfoque permite que la función de un setter limpie la entrada sin destruir el tipo del getter (es decir, verse obligado a recurrir al uso de cualquiera).

Para que el getter sea un subconjunto del tipo del setter, creo que está innecesariamente limitado. En realidad, el tipo de un setter _podría_ teóricamente ser literalmente cualquier cosa, siempre que la implementación siempre devuelva un subtipo del tipo del getter. Requerir que el setter sea un supertipo del tipo getter es una especie de limitación extraña que podría cumplir con el par de casos de uso presentados en este ticket, pero seguramente generaría quejas más adelante.

Dicho esto, las clases son implícitamente también interfaces, y los getters y setters son esencialmente detalles de implementación que no aparecerían en una interfaz. En el ejemplo de OP, terminaría con type MyClass = { myDate(): moment.Moment; } . Tendría que exponer de alguna manera estos nuevos tipos getter y setter como parte de la interfaz, aunque personalmente no veo por qué eso sería deseable.

Este problema básicamente solicita una versión menos limitada de la sobrecarga del operador de = y === . (Los getters y setters ya son sobrecargas de operadores). Y como alguien que ha lidiado con sobrecargas de operadores en otros lenguajes, diré que creo que deben evitarse en la mayoría de los casos. Claro, alguien podría sugerir algún ejemplo matemático como puntos polares y cartesianos, pero en la gran mayoría de los casos en los que usaría TS (incluidos los ejemplos en este hilo), diría que necesitar una sobrecarga de operadores es probablemente un olor a código. Puede parecer que simplifica la API, pero tiende a hacer lo contrario. Como se mencionó anteriormente, hacer que lo siguiente no sea necesariamente cierto es súper confuso y poco intuitivo.

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

(Creo que las únicas veces que he visto el uso de setters que parecían razonables son para iniciar sesión y establecer una bandera "sucia" en los objetos para que otra cosa pueda decir si un campo ha cambiado. Ninguno de estos realmente cambia el contrato del objeto.)

@MikeyBurkman

[...] pero en la gran mayoría de los casos en los que usaría TS (incluidos los ejemplos en este hilo), diría que necesitar una sobrecarga de operadores es probablemente un olor a código [...]

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

Por lo tanto, simplemente hacer que los tipos coincidan no evita un comportamiento sorprendente y, de hecho, el.style.display = ''; //so is this now true: el.style.display === ''? muestra que eso tampoco es teórico. Incluso con campos simples y antiguos, la suposición no se cumple para NaN, por cierto.

Pero lo que es más importante, su argumento ignora el punto de que TS realmente no puede opinar sobre estas cosas porque TS necesita interoperar con las API de JS existentes, incluidas las principales y que es poco probable que cambien cosas como el DOM. Y como tal, simplemente no importa si la API es ideal o no; lo que importa es que TS no puede interoperar limpiamente con dichas API dadas . Ahora, se ve obligado a volver a implementar cualquier lógica alternativa que la API haya utilizado internamente para forzar un valor de tipo out-of-getter pasado al setter y, por lo tanto, escribir la propiedad como el tipo de getters, o ignorar la verificación de tipo para agregar aserciones de tipos inútiles en cada sitio de getters y, por lo tanto, escriba la propiedad como el tipo de setters. O, peor aún: haga lo que sea que TS elija anotar para el DOM, independientemente de si eso es ideal.

Todas esas elecciones son malas. La única manera de evitar eso es aceptar la necesidad de representar JS apis lo más fielmente posible, y aquí: eso parece posible (ciertamente sin ningún conocimiento de las partes internas de TS que pueden hacer que esto sea decididamente no trivial).

eso parece posible (es cierto que sin ningún conocimiento de las partes internas de TS que pueden hacer que esto sea decididamente no trivial)

Sospecho que parte de la nueva sintaxis de declaración de tipo que habilitaron más recientemente podría hacer esto expresable cuando antes era menos factible, por lo que realmente deberían considerar reabrir este problema... @RyanCavanaugh

En un mundo ideal, TS interoperaría con todas las API de JS. Pero ese no es el caso. Hay un montón de modismos de JS que no se pueden escribir correctamente en TS. Sin embargo, prefiero que se concentren en arreglar cosas que realmente conducen a mejores lenguajes de programación, no peores. Si bien apoyar esto de una manera sensata sería bueno, hay una docena de otras cosas en las que personalmente preferiría que dedicaran su tiempo limitado primero. Esto está muy abajo en esa lista.

@MikeyBurkman , la cuestión de la prioridad es válida. Mi principal preocupación es la decisión de @RyanCavanaugh de cerrar este problema y cancelarlo. Descartar todos los problemas señalados en este hilo entra en conflicto directamente con la misión declarada de Typescript, que es ser un "superconjunto" de Javascript (en lugar de un subconjunto).

Sí, definitivamente puedo estar de acuerdo en que este problema probablemente no debería cerrarse, ya que es algo que _probablemente_ debería arreglarse con el tiempo. (Aunque dudo mucho que lo sea).

Si bien sería bueno apoyar esto de una manera sensata, hay una docena de otras cosas en las que personalmente preferiría que dedicaran su tiempo limitado primero.

Creo que está subestimando un aspecto importante de TypeScript en el sentido de que se supone que hace que sea más seguro usar las API DOM existentes y las API JS existentes. Existe para mantener algo más que su propio código, dentro de su burbuja, seguro.

Este es mi punto de vista, construyo bibliotecas de componentes, y aunque no necesariamente causaría deliberadamente que exista esta falta de coincidencia entre setters/getters dadas mis preferencias. A veces mi mano se ve forzada en función de cómo mis bibliotecas necesitan interactuar con otros sistemas en el lugar. Por ejemplo, Angular establece todas las propiedades de entrada en un componente que proviene del marcado como cadenas, en lugar de hacer cualquier tipo de coerción en función de su conocimiento del tipo de destino (al menos, la última vez que lo verifiqué). Entonces, ¿te niegas a aceptar cadenas? ¿Convierte todo en una cadena, aunque esto haría que sus tipos fueran horribles de usar? ¿O haces lo que TypeScript te pediría que hicieras y usas un tipo como: string | Colorea pero haz que usar los getters sea horrible. Todas estas son opciones bastante terribles que reducen la seguridad, cuando algo de expresividad adicional del sistema de tipos habría ayudado.

El problema es que no es solo Angular lo que causa estos problemas. Angular terminó en esa situación porque refleja muchos escenarios en el DOM donde ocurre la autocoerción en un conjunto de propiedades, pero el getter siempre es un tipo singular anticipado.

Mire un poco hacia arriba: Angular es mucho peor de lo que piensa .

Para mí, suele ser el problema cuando desea crear un envoltorio alrededor de un almacenamiento de propósito general, que puede almacenar pares clave-valor y desea limitar a los usuarios a ciertas claves.

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

Desea tener tal restricción: el usuario puede obtener null , si el valor no se estableció previamente, pero no puede establecerlo en null . Actualmente, no puede hacerlo, porque el tipo de entrada del setter debe incluir null .

@fan-tom, ¡excelente caso de uso de por qué este problema debe reabrirse! Sigue viniendo.

¡Este problema tiene un número extremadamente alto de votos a favor!

Este es el proyecto del equipo de TS, por lo que pueden hacer lo que mejor les parezca como les guste. Pero si su objetivo es hacer que esto sea lo más útil posible para una comunidad externa de usuarios, entonces espero que el equipo de TS pueda tomar en consideración la gran cantidad de votos de la comunidad.

La página de inicio de typescriptlang.org actualmente es inexacta cuando dice:

TypeScript es un superconjunto escrito de JavaScript que se compila en JavaScript simple.

TypeScript es un _superconjunto_ con tipo de un _subconjunto_ de JavaScript que se compila en JavaScript simple. :smiley:

TypeScript es un superconjunto escrito de un subconjunto de JavaScript que se compila en JavaScript simple.

Más diplomáticamente, creo que se podría decir que es un superconjunto obstinado de JavaScript, cuando el equipo hace este tipo de omisión obstinada del modelado del sistema de tipos.

Están diciendo, "no modelaremos esto, porque es difícil para nosotros hacerlo, y creemos que no deberías hacerlo, de todos modos". Pero JavaScript está repleto de cosas que _no_ deberías_ hacer y, en general, Typescript no te impedirá hacer estas cosas si tienes la voluntad y el conocimiento, porque parece que la estrategia general es no ser obstinado sobre lo que JavaScript puede y no puede simplemente ejecutar como Typescript.

Es por eso que es tan extraño negarse a modelar este escenario común (¡usado en el DOM!), Citando la creencia de que las API no deberían realizar coerción basada en setter como la razón fundamental para no hacerlo.

Actualmente, esto no parece ser posible, y tengo que recurrir a algo como esto:

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

Lo mejor que puede hacer es agregar un constructor y llamar a la función de configuración personalizada allí, si esa es la única vez que establece ese valor.

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

El problema a la hora de asignar valores directamente sigue existiendo.

¿Alguna actualización sobre esto, después de más de 5,5 años?

@xhliu como indican las etiquetas, es una limitación de diseño que es demasiado compleja de abordar y, por lo tanto, el problema está cerrado. No esperaría una actualización. El período de tiempo para un problema cerrado es algo irrelevante.

Vuelva a abrir. También esto debe agregarse en el nivel de la interfaz. Un ejemplo clásico es cuando implementa un Proxy donde tiene total flexibilidad sobre lo que puede leer y escribir.

@xhliu ... demasiado complejo para abordar ...

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

const testObj = {
    foo: "bar"
}

testObj.foo

El símbolo foo dice esto:

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

Aquí hay mucho espacio para información adicional, lo cual es un gran diseño en nombre de TS. Quien haya diseñado esto probablemente lo hizo específicamente para hacer posibles características adicionales (incluso grandes) en el futuro.

Especulativo a partir de este momento:

Si fuera posible declarar (por ejemplo de pseudocódigo) un accessDelclaration: AccessSpecification (foo) , entonces el PropertyAccessExpression que es consciente del símbolo foo y sus declaraciones, podría verificar condicionalmente si hay un "accessDelclaration" y use la escritura de eso en su lugar.

Suponiendo que exista la sintaxis para agregar esa propiedad accessDelclaration al símbolo "foo", PropertyAccessExpression debería poder extraer la "Especificación de acceso" del símbolo que obtiene de ts.createIdentifier("foo") y producir diferentes tipos.

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

Especulativamente, parece que la parte más difícil de este desafío probablemente sea la cantidad de cambios en torno a la sintaxis (¿o tal vez una filosofía de la empresa?), pero desde una perspectiva de implementación, todas las herramientas deberían estar allí. Se agregaría una sola condición a la función ts.createPropertyAccess() , y se debe agregar una clase de Declaración para representar esa condición y sus efectos al símbolo de la propiedad del objeto.

Se han escrito muchos buenos ejemplos de por qué esto se necesita desesperadamente (especialmente para DOM y Angular).

Solo agregaré que me golpeó esto hoy cuando migré el antiguo código JS a TS donde la asignación string a window.location no funcionó y tuve que hacer una solución alternativa as any 😟

La propiedad de solo lectura Window.location devuelve un objeto Location con información sobre la ubicación actual del documento.

Aunque Window.location es un objeto Location de solo lectura, también puede asignarle un DOMString. Esto significa que puede trabajar con la ubicación como si fuera una cadena en la mayoría de los casos: ubicación = ' http://www.ejemplo.com ' es sinónimo de ubicación.href = ' http://www.ejemplo.com ' .
fuente

migrar el código JS antiguo a TS donde la asignación string a window.location no funcionó y tuve que hacer una solución alternativa as any

Ese es un gran ejemplo.

TS necesita esto. Esta es una parte muy normal de JavaScript.

Veo que el tema actual fue cerrado. Pero, según tengo entendido, ¿esta funcionalidad no se realizó?

@AGluk , este problema debe reabrirse.

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

Temas relacionados

uber5001 picture uber5001  ·  3Comentarios

Antony-Jones picture Antony-Jones  ·  3Comentarios

jbondc picture jbondc  ·  3Comentarios

blendsdk picture blendsdk  ·  3Comentarios

dlaberge picture dlaberge  ·  3Comentarios