Como um problema de continuação #2947, que permite o modificador abstract
em declarações de métodos, mas não o permite em declarações de métodos estáticos, sugiro expandir essa funcionalidade para declarações de métodos estáticos, permitindo o modificador abstract static
em declarações de métodos .
O problema relacionado diz respeito ao modificador static
na declaração de métodos de interface, que não é permitido.
Em alguns casos de uso da classe abstrata e suas implementações, pode ser necessário ter alguns valores dependentes de classe (não dependentes de instância), que devem ser acessados dentro do contexto da classe filha (não dentro do contexto de um objeto), sem criando um objeto. O recurso que permite fazer isso é o modificador static
na declaração do método.
Por exemplo (exemplo 1):
abstract class AbstractParentClass {
}
class FirstChildClass extends AbstractParentClass {
public static getSomeClassDependentValue(): string {
return 'Some class-dependent value of class FirstChildClass';
}
}
class SecondChildClass extends AbstractParentClass {
public static getSomeClassDependentValue(): string {
return 'Some class-dependent value of class SecondChildClass';
}
}
FirstChildClass.getSomeClassDependentValue(); // returns 'Some class-dependent value of class FirstChildClass'
SecondChildClass.getSomeClassDependentValue(); // returns 'Some class-dependent value of class SecondChildClass'
Mas em alguns casos também preciso acessar esse valor quando só sei que a classe de acesso é herdada de AbstractParentClass, mas não sei qual classe filha específica estou acessando. Então, quero ter certeza de que todo filho da AbstractParentClass tenha esse método estático.
Por exemplo (exemplo 2):
abstract class AbstractParentClass {
}
class FirstChildClass extends AbstractParentClass {
public static getSomeClassDependentValue(): string {
return 'Some class-dependent value of class FirstChildClass';
}
}
class SecondChildClass extends AbstractParentClass {
public static getSomeClassDependentValue(): string {
return 'Some class-dependent value of class SecondChildClass';
}
}
abstract class AbstractParentClassFactory {
public static getClasses(): (typeof AbstractParentClass)[] {
return [
FirstChildClass,
SecondChildClass
];
}
}
var classes = AbstractParentClassFactory.getClasses(); // returns some child classes (not objects) of AbstractParentClass
for (var index in classes) {
if (classes.hasOwnProperty(index)) {
classes[index].getSomeClassDependentValue(); // error: Property 'getSomeClassDependentValue' does not exist on type 'typeof AbstractParentClass'.
}
}
Como resultado, o compilador decide que ocorreu um erro e exibe a mensagem: A propriedade 'getSomeClassDependentValue' não existe no tipo 'typeof AbstractParentClass'.
Em alguns casos, a lógica da interface implica que as classes implementadoras devem ter um método estático, que possui a lista predeterminada de parâmetros e retorna o valor do tipo exato.
Por exemplo (exemplo 3):
interface Serializable {
serialize(): string;
static deserialize(serializedValue: string): Serializable; // error: 'static' modifier cannot appear on a type member.
}
Ao compilar esse código, ocorre um erro: o modificador 'static' não pode aparecer em um membro do tipo.
A solução para ambos os problemas (1.1 e 1.2) é permitir o modificador abstract
em declarações de métodos estáticos em classes abstratas e o modificador static
em interfaces.
A implementação desse recurso em JavaScript deve ser semelhante à implementação de interfaces, métodos abstratos e métodos estáticos.
Isso significa que:
Por exemplo, este código TypeScript (exemplo 4):
interface Serializable {
serialize(): string;
static deserialize(serializedValue: string): Serializable;
}
abstract class AbstractParentClass {
public abstract static getSomeClassDependentValue(): string;
}
class FirstChildClass extends AbstractParentClass {
public static getSomeClassDependentValue(): string {
return 'Some class-dependent value of class FirstChildClass';
}
}
class SecondChildClass extends AbstractParentClass implements Serializable {
public serialize(): string {
var serialisedValue: string;
// serialization of this
return serialisedValue;
}
public static deserialize(serializedValue: string): SecondChildClass {
var instance = new SecondChildClass();
// deserialization
return instance;
}
public static getSomeClassDependentValue(): string {
return 'Some class-dependent value of class SecondChildClass';
}
}
deve ser compilado para este código JS:
var AbstractParentClass = (function () {
function AbstractParentClass() {
}
return AbstractParentClass;
}());
var FirstChildClass = (function (_super) {
__extends(FirstChildClass, _super);
function FirstChildClass() {
return _super !== null && _super.apply(this, arguments) || this;
}
FirstChildClass.getSomeClassDependentValue = function () {
return 'Some class-dependent value of class FirstChildClass';
};
return FirstChildClass;
}(AbstractParentClass));
var SecondChildClass = (function (_super) {
__extends(SecondChildClass, _super);
function SecondChildClass() {
return _super !== null && _super.apply(this, arguments) || this;
}
SecondChildClass.prototype.serialize = function () {
var serialisedValue;
// serialization of this
return serialisedValue;
};
SecondChildClass.deserialize = function (serializedValue) {
var instance = new SecondChildClass();
// deserialization
return instance;
};
SecondChildClass.getSomeClassDependentValue = function () {
return 'Some class-dependent value of class SecondChildClass';
};
return SecondChildClass;
}(AbstractParentClass));
abstract static
abstract static
ou static
static
static
Todas as outras propriedades do modificador abstract static
devem ser herdadas das propriedades dos modificadores abstract
e static
.
Todas as outras propriedades do modificador de método de interface static
devem ser herdadas dos métodos de interface e propriedades do modificador static
.
abstract static
modificador de um método de classe abstrata e static
modificador de um método de interface.abstract static
de um método de classe abstrata e o modificador static
de um método de interface. Métodos de interface estática geralmente não fazem sentido, veja #13462
Segurando isso até ouvirmos mais comentários sobre isso.
Uma questão importante que tivemos ao considerar isso: Quem tem permissão para chamar um método abstract static
? Presumivelmente, você não pode invocar AbstractParentClass.getSomeClassDependentValue
diretamente. Mas você pode invocar o método em uma expressão do tipo AbstractParentClass
? Se sim, por que isso deveria ser permitido? Se não, qual é o uso do recurso?
Métodos de interface estática geralmente não fazem sentido, veja #13462
Na discussão de #13462 eu não vi porque os métodos de interface static
são sem sentido. Só vi que sua funcionalidade pode ser implementada por outros meios (o que prova que não são sem sentido).
Do ponto de vista prático, a interface é um tipo de especificação - um determinado conjunto de métodos que são obrigatórios para sua implementação em uma classe que implementa essa interface. A interface não apenas define a funcionalidade que um objeto fornece, mas também é um tipo de contrato.
Portanto, não vejo nenhuma razão lógica para que class
tenha o método static
e interface
não.
Se seguirmos o ponto de vista de que tudo o que já pode ser implementado na linguagem não precisa de melhorias de sintaxe e outras coisas (ou seja, este argumento foi um dos pontos principais na discussão do #13462), então guiado por este ponto de vista, podemos decidir que o ciclo while
é redundante porque pode ser implementado usando for
e if
juntos. Mas não vamos acabar com while
.
Uma questão importante que tivemos ao considerar isso: Quem tem permissão para chamar um método
abstract static
? Presumivelmente, você não pode invocarAbstractParentClass.getSomeClassDependentValue
diretamente. Mas você pode invocar o método em uma expressão do tipoAbstractParentClass
? Se sim, por que isso deveria ser permitido? Se não, qual é o uso do recurso?
Boa pergunta. Já que você estava considerando esse problema, você poderia compartilhar suas ideias sobre isso?
Só me vem à mente que no nível do compilador o caso de chamada direta de AbstractParentClass.getSomeClassDependentValue
não será rastreado (porque não pode ser rastreado), e ocorrerá o erro de tempo de execução do JS. Mas não tenho certeza se isso é consistente com a ideologia do TypeScript.
Só vi que sua funcionalidade pode ser implementada por outros meios (o que prova que não são sem sentido).
Só porque algo é implementável, não significa que faça sentido. 😉
abstract class Serializable {
abstract serialize (): Object;
abstract static deserialize (Object): Serializable;
}
Eu quero forçar a implementação do método deserialize estático nas subclasses de Serializable.
Existe alguma solução alternativa para implementar esse comportamento?
Qual é a última opinião sobre isso? Eu apenas tentei escrever uma propriedade estática abstrata de uma classe abstrata e fiquei genuinamente surpreso quando isso não foi permitido.
O que @patryk-zielinski93 disse. Vindo de alguns anos de projetos em PHP que convertemos para TS, queremos métodos estáticos em interfaces.
Um caso de uso muito comum com o qual isso ajudará são os componentes React, que são classes com propriedades estáticas como displayName
, propTypes
e defaultProps
.
Devido a essa limitação, as tipagens para React atualmente incluem dois tipos: uma classe Component
ComponentClass
incluindo a função construtora e propriedades estáticas.
Para verificar o tipo completo de um componente React com todas as suas propriedades estáticas, é preciso usar os dois tipos.
Exemplo sem ComponentClass
: propriedades estáticas são ignoradas
import React, { Component, ComponentClass } from 'react';
type Props = { name: string };
{
class ReactComponent extends Component<Props, any> {
// expected error, but got none: displayName should be a string
static displayName = 1
// expected error, but got none: defaultProps.name should be a string
static defaultProps = { name: 1 }
};
}
Exemplo com ComponentClass
: propriedades estáticas são verificadas por tipo
{
// error: displayName should be a string
// error: defaultProps.name should be a string
const ReactComponent: ComponentClass<Props> = class extends Component<Props, any> {
static displayName = 1
static defaultProps = { name: 1 }
};
}
Suspeito que muitas pessoas não estejam usando ComponentClass
, sem saber que suas propriedades estáticas não estão sendo verificadas por tipo.
Problema relacionado: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16967
Há algum progresso nisso? Ou alguma solução alternativa para construtores em classes abstratas?
Aqui está um exemplo:
abstract class Model {
abstract static fromString(value: string): Model
}
class Animal extends Model {
constructor(public name: string, public weight: number) {}
static fromString(value: string): Animal {
return new Animal(...JSON.parse(value))
}
}
@roboslone
class Animal {
static fromString(value: string): Animal {
return new Animal();
}
}
function useModel<T>(model: { fromString(value: string): T }): T {
return model.fromString("");
}
useModel(Animal); // Works!
Concordou que este é um recurso extremamente poderoso e útil. Na minha opinião, essa característica é o que torna as classes 'cidadãs de primeira classe'. A herança de métodos de classe/estáticos pode e faz sentido, particularmente para o padrão de fábrica de métodos estáticos, que foi citado aqui por outros pôsteres várias vezes. Esse padrão é particularmente útil para desserialização, que é uma operação executada com frequência no TypeScript. Por exemplo, faz todo o sentido querer definir uma interface que forneça um contrato declarando que todos os tipos de implementação são instanciados a partir de JSON.
Não permitir métodos de fábrica estáticos abstratos exige que o implementador crie classes de fábrica abstratas, duplicando desnecessariamente o número de definições de classe. E, como outros pôsteres apontaram, esse é um recurso poderoso e bem-sucedido implementado em outras linguagens, como PHP e Python.
Novo no Typescript, mas também estou surpreso que isso não seja permitido por padrão e que tantas pessoas estejam tentando justificar a não adição do recurso:
Outro caso de uso simples: (maneira ideal, que não funciona)
import {map} from 'lodash';
export abstract class BaseListModel {
abstract static get instance_type();
items: any[];
constructor(items?: any[]) {
this.items = map(items, (item) => { return new this.constructor.instance_type(item) });
}
get length() { return this.items.length; }
}
export class QuestionList extends BaseListModel {
static get instance_type() { return Question }
}
Em vez disso, a instância de lista acaba expondo o tipo de instância diretamente, o que não é relevante para a própria instância de lista e deve ser acessado por meio do construtor. Parece sujo. Pareceria muito mais sujo se estivéssemos falando de um modelo de registro, no qual especificamos um conjunto de valores padrão por meio do mesmo tipo de mecanismo etc.
Eu estava muito animado para usar classes abstratas reais em uma linguagem depois de estar em Ruby/javascript por tanto tempo, apenas para acabar desanimado com as restrições de implementação - O exemplo acima foi apenas meu primeiro exemplo de encontrá-lo, embora eu possa pensar em muitos outros casos de uso em que seria útil. Principalmente, apenas como um meio de criar DSL's/configurações simples como parte da interface estática, garantindo que uma classe especifique um objeto de valores padrão ou o que quer que seja. - E você pode estar pensando, bem, é para isso que servem as interfaces. Mas para algo simples como isso, não faz muito sentido, e só acaba tornando as coisas mais complicadas do que precisam ser (a subclasse precisaria estender a classe abstrata e implementar alguma interface, torna a nomeação das coisas mais complicada, etc).
Eu tive esse requisito semelhante para o meu projeto duas vezes. Ambos foram relacionados para garantir que todas as subclasses forneçam implementações concretas de um conjunto de métodos estáticos. Meu cenário está descrito abaixo:
class Action {
constructor(public type='') {}
}
class AddAppleAction extends Action {
static create(apple: Apple) {
return new this(apple);
}
constructor(public apple: Apple) {
super('add/apple');
}
}
class AddPearAction extends Action {
static create(pear: Pear) {
return new this(pear);
}
constructor(public pear: Pear) {
super('add/pear');
}
}
const ActionCreators = {
addApple: AddAppleAction
addPear: AddPearAction
};
const getActionCreators = <T extends Action>(map: { [key: string]: T }) => {
return Object.entries(map).reduce((creators, [key, ActionClass]) => ({
...creators,
// To have this function run properly,
// I need to guarantee that each ActionClass (subclass of Action) has a static create method defined.
// This is where I want to use abstract class or interface to enforce this logic.
// I have some work around to achieve this by using interface and class decorator, but it feels tricky and buggy. A native support for abstract class method would be really helpful here.
[key]: ActionClass.create.bind(ActionClass)
}), {});
};
Espero que isso possa explicar meus requisitos. Obrigado.
Para quem procura uma solução alternativa , você pode usar este decorador:
class myClass {
public classProp: string;
}
interface myConstructor {
new(): myClass;
public readonly staticProp: string;
}
function StaticImplements <T>() {
return (constructor: T) => { };
}
<strong i="7">@StaticImplements</strong> <myConstructor>()
class myClass implements myClass {}
const getActionCreators = <T extends Action>(map: { [key: string]: {new () => T, create() : T}}) => {
como estamos chamando por meio de um parâmetro de tipo, a classe base real com seu método de fábrica hipotético abstract
static
nunca estará envolvida. Os tipos das subclasses são relacionados estruturalmente quando o parâmetro type é instanciado.
O que não estou vendo nesta discussão são casos em que a implementação é invocada por meio do tipo da classe base em oposição a algum tipo sintetizado.
Também é relevante considerar que, diferentemente de linguagens como C#, onde os membros abstract
são realmente _substituídos_ por suas implementações em classes derivadas, implementações de membros JavaScript, instância ou outra, nunca substituem números herdados, mas sim _sombra_ eles.
Meus 5 centavos aqui, do ponto de vista da perspectiva do usuário, são:
Para o caso de interfaces, deve ser permitido o modificador _static_
Interfaces definem contratos, a serem cumpridos pela implementação de classes. Isso significa que as interfaces são abstrações em um nível mais alto do que as classes.
Neste momento o que posso ver é que as classes podem ser mais expressivas que as interfaces, de forma que podemos ter método estático nas classes mas não podemos impor isso, a partir da própria definição do contrato.
Na minha opinião, isso parece errado e atrapalhando.
Existe algum histórico técnico que torna esse recurso de linguagem difícil ou impossível de implementar?
Felicidades
Interfaces definem contratos, a serem cumpridos pela implementação de classes. Isso significa que as interfaces são abstrações em um nível mais alto do que as classes.
As classes têm duas interfaces, dois contratos de implementação, e não há como fugir disso.
Existem razões pelas quais linguagens como C# também não possuem membros estáticos para interfaces. Logicamente, as interfaces são a superfície pública de um objeto. Uma interface descreve a superfície pública de um objeto. Isso significa que não contém nada que não esteja lá. Em instâncias de classes, métodos estáticos não estão presentes na instância. Eles só existem na função classe/construtor, portanto devem ser descritos apenas nessa interface.
Em instâncias de classes, métodos estáticos não estão presentes na instância.
você pode detalhar isso? Isso não é C#
Eles só existem na função classe/construtor, portanto devem ser descritos apenas nessa interface.
Que conseguimos e é isso que queremos mudar.
Interfaces definem contratos, a serem cumpridos pela implementação de classes. Isso significa que as interfaces são abstrações em um nível mais alto do que as classes.
Eu concordo plenamente com isso. Veja o exemplo de interface Json
Oi @kitsonk , por favor, você poderia elaborar mais sobre:
As classes têm duas interfaces, dois contratos de implementação, e não há como fugir disso.
Eu não entendi essa parte.
Logicamente, as interfaces são a superfície pública de um objeto. Uma interface descreve a superfície pública de um objeto. Isso significa que não contém nada que não esteja lá.
Eu concordo. Não vejo nenhuma contradição no que eu disse. Eu até disse mais. Eu disse que uma interface é um contrato para que uma classe seja cumprida.
Em instâncias de classes, métodos estáticos não estão presentes na instância. Eles só existem na função classe/construtor, portanto devem ser descritos apenas nessa interface.
Não tenho certeza se entendi direito, está claro o que diz, mas não por que você diz. Eu posso explicar porque eu acho que ainda é válida a minha declaração. Estou passando interfaces como parâmetros o tempo todo para meus métodos, isso significa que tenho acesso a métodos de interface, observe que não estou usando interfaces aqui como uma maneira de definir a estrutura de dados, mas para definir objetos concretos que são criados/hidratados em outro lugar . Então quando eu tenho:
fetchData(account: SalesRepresentativeInterface): Observable<Array<AccountModel>> {
// Method Body
}
Nesse corpo de método, posso usar métodos account
. O que eu gostaria que fosse possível fazer é poder usar métodos estáticos de SalesRepresentativeInterface
que já foram aplicados para serem implementados em qualquer que seja a classe que estou recebendo em account
. Talvez eu esteja tendo uma ideia muito simplista ou completamente errada sobre como usar o recurso.
Acho que permitir o modificador static
me permitirá fazer algo como: SalesRepresentativeInterface.staticMethodCall()
Estou errado ?
Felicidades
@davidmpaz : sua sintaxe não está certa, mas está próxima. Aqui está um exemplo de padrão de uso:
interface JSONSerializable {
static fromJSON(json: any): JSONSerializable;
toJSON(): any;
}
function makeInstance<T extends JSONSerializable>(cls: typeof T): T {
return cls.fromJSON({});
}
class ImplementsJSONSerializable implements JSONSerializable {
constructor(private json: any) {
}
static fromJSON(json: any): ImplementsJSONSerializable {
return new ImplementsJSONSerializable(json);
}
toJSON(): any {
return this.json;
}
}
// returns an instance of ImplementsJSONSerializable
makeInstance(ImplementsJSONSerializable);
Infelizmente, para que isso funcione, precisamos de dois recursos do TypeScript: (1) métodos estáticos em interfaces e métodos estáticos abstratos; (2) a capacidade de usar typeof
como uma dica de tipo com classes genéricas.
@davidmpaz
Eu não entendi essa parte.
As classes têm duas interfaces. A função construtora e o protótipo da instância. A palavra-chave class
é essencialmente açúcar para isso.
interface Foo {
bar(): void;
}
interface FooConstructor {
new (): Foo;
prototype: Foo;
baz(): void;
}
declare const Foo: FooConstructor;
const foo = new Foo();
foo.bar(); // instance method
Foo.baz(); // static method
@jimmykane
você pode detalhar isso? Isso não é C#
class Foo {
bar() {}
static baz() {}
}
const foo = new Foo();
foo.bar();
Foo.baz();
Não há .baz()
em instâncias de Foo
. .baz()
existe apenas no construtor.
De uma perspectiva de tipo, você pode referenciar/extrair essas duas interfaces. Foo
refere-se à interface da instância pública e typeof Foo
refere-se à interface do construtor público (que inclui os métodos estáticos).
Para acompanhar a explicação do @kitsonk , aqui está o exemplo acima reescrito para alcançar o que você deseja:
interface JSONSerializable <T>{
fromJSON(json: any): T;
}
function makeInstance<T>(cls: JSONSerializable<T>): T {
return cls.fromJSON({});
}
class ImplementsJSONSerializable {
constructor (private json: any) {
}
static fromJSON(json: any): ImplementsJSONSerializable {
return new ImplementsJSONSerializable(json);
}
toJSON(): any {
return this.json;
}
}
// returns an instance of ImplementsJSONSerializable
makeInstance(ImplementsJSONSerializable);
Observe aqui que:
implements
não é realmente necessária, toda vez que você usa a classe, uma verificação estrutural é feita e a classe será validada para corresponder à interface necessária.typeof
para obter o tipo de construtor, apenas certifique-se de que seu T
seja o que você pretende capturar@mhegazy : você teve que remover minha chamada toJSON
na interface JSONSerializable. Embora minha função de exemplo simples makeInstance
chame apenas fromJSON
, é muito comum querer instanciar um objeto e usá-lo. Você removeu minha capacidade de fazer chamadas sobre o que é retornado por makeInstance
, porque eu realmente não sei o que T
está dentro makeInstance
(particularmente relevante se makeInstance
é um método de classe usado internamente para criar uma instância, então use-o). Claro que eu poderia fazer isso:
interface JSONSerializer {
toJSON(): any;
}
interface JSONSerializable <T extends JSONSerializer> {
fromJSON(json: any): T;
}
function makeInstance<T extends JSONSerializer>(cls: JSONSerializable<T>): T {
return cls.fromJSON({});
}
class ImplementsJSONSerializer implements JSONSerializer {
constructor (private json: any) {
}
static fromJSON(json: any): ImplementsJSONSerializer {
return new ImplementsJSONSerializer(json);
}
toJSON(): any {
return this.json;
}
}
// returns an instance of ImplementsJSONSerializable
makeInstance(ImplementsJSONSerializer);
E agora eu sei que meu T
terá todos os métodos disponíveis em JSONSerializer
. Mas isso é excessivamente detalhado e difícil de raciocinar (espere, você está passando ImplementsJSONSerializer
, mas isso não é um JSONSerializable
, é? Espere, você está digitando de pato??) . O mais fácil de raciocinar sobre a versão é:
interface JSONSerializer {
toJSON(): any;
}
interface JSONSerializable <T extends JSONSerializer> {
fromJSON(json: any): T;
}
function makeInstance<T extends JSONSerializer>(cls: JSONSerializable<T>): T {
return cls.fromJSON({});
}
class ImplementsJSONSerializer implements JSONSerializer {
constructor (private json: any) {
}
toJSON(): any {
return this.json;
}
}
class ImplementsJSONSerializable implements JSONSerializable<ImplementsJSONSerializer> {
fromJSON(json: any): ImplementsJSONSerializer {
return new ImplementsJSONSerializer(json);
}
}
// returns an instance of ImplementsJSONSerializable
makeInstance(new ImplementsJSONSerializable());
Mas, como apontei em um comentário anterior, agora devemos criar uma classe de fábrica para cada classe que queremos instanciar, o que é ainda mais detalhado do que o exemplo de digitação de pato. Esse é certamente um padrão viável feito em Java o tempo todo (e presumo que C# também). Mas é desnecessariamente verboso e duplicativo. Todo esse clichê desaparece se métodos estáticos são permitidos em interfaces e métodos estáticos abstratos são permitidos em classes abstratas.
não há necessidade da classe adicional. classes podem ter membros static
.. você só não precisa da cláusula implements
. em um sistema de tipo nominal, você realmente precisa dele para afirmar o relacionamento "é um" em sua classe. em um sistema de tipo estrutural, você realmente não precisa disso. o relacionamento "é um" é verificado em cada uso de qualquer maneira, para que você possa descartar com segurança a cláusula implements
e não perder a segurança.
então, em outras palavras, você pode ter uma classe que tenha um fromJSON
estático e cujas instâncias tenham um toJSON
:
interface JSONSerializer {
toJSON(): any;
}
interface JSONSerializable<T extends JSONSerializer> {
fromJSON(json: any): T;
}
function makeInstance<T extends JSONSerializer>(cls: JSONSerializable<T>): T {
return cls.fromJSON({});
}
class ImplementsJSONSerializer {
constructor (private json: any) {
}
static fromJSON(json: any): ImplementsJSONSerializer {
return new ImplementsJSONSerializer(json);
}
toJSON(): any {
return this.json;
}
}
// returns an instance of ImplementsJSONSerializable
makeInstance(ImplementsJSONSerializer);
@mhegazy Eu já indiquei isso como uma opção viável. Veja meu primeiro exemplo de 'digitação de pato'. Argumentei que é desnecessariamente verboso e difícil de raciocinar. Afirmei então que a versão mais fácil de raciocinar (classes de fábrica) é ainda mais detalhada.
Ninguém discorda que seja possível contornar a falta de métodos estáticos nas interfaces. Na verdade, eu diria que há uma solução mais elegante usando estilos funcionais em vez de classes de fábrica, embora essa seja minha preferência pessoal.
Mas, na minha opinião, a maneira mais limpa e fácil de raciocinar sobre a implementação desse padrão excepcionalmente comum é usar métodos estáticos abstratos. Aqueles de nós que adoram esse recurso em outras linguagens (Python, PHP) sentem falta de tê-lo no TypeScript. Obrigado pelo seu tempo.
Afirmei então que a versão mais fácil de raciocinar (classes de fábrica)
não tenho certeza se concordo que isso é mais fácil de raciocinar.
Mas, na minha opinião, a maneira mais limpa e fácil de raciocinar sobre a implementação desse padrão excepcionalmente comum é usar métodos estáticos abstratos. Aqueles de nós que adoram esse recurso em outras linguagens (Python, PHP) sentem falta de tê-lo no TypeScript.
E esse problema está rastreando a adição disso. Eu só queria garantir que os futuros visitantes deste tópico entendam que isso é possível hoje sem uma cláusula implements
.
@kitsonk certo, eu perdi isso.
Pessoal, então eu tenho como LocationModule, que deve ter método para obter opções para armazenar em banco de dados, a partir do qual ele deve se recriar.
Cada LocationModule tem suas próprias opções com tipo para recreação.
Então agora eu tenho que criar uma fábrica com genéricos para recreação e mesmo assim não tenho verificações de tempo de compilação. Você sabe, batendo o propósito aqui.
No começo eu estava tipo "bem, sem estática para interfaces, então vamos ficar com a classe abstrata, MAS até isso seria sujo, já que agora eu tenho que mudar em todos os lugares do meu tipo de código de interface para classe abstrata para que o verificador reconheça os métodos que estou procurando .
Aqui:
export interface LocationModuleInterface {
readonly name: string;
getRecreationOptions(): ModuleRecreationOptions;
}
export abstract class AbstractLocationModule implements LocationModuleInterface {
abstract readonly name: string;
abstract getRecreationOptions(): ModuleRecreationOptions;
abstract static recreateFromOptions(options: ModuleRecreationOptions): AbstractLocationModule;
}
E então eu tropeço bem, estático não pode ser com abstrato. E não pode estar na interface.
Pessoal, sério, não estamos protegendo não implementar isso apenas por não implementar isso?
Eu amo TypeScript apaixonadamente. Como louco, quero dizer. Mas há sempre mas.
Em vez de forçar a implementação do método estático, eu teria que verificar novamente como estava fazendo com tudo em JavaScript simples e antigo.
A arquitetura já está cheia de padrões, decisões legais etc, mas alguns deles são apenas para superar problemas com o uso de coisas tão simples como o método estático da interface.
Minha fábrica terá que verificar em tempo de execução a existência do método estático. Como é isso?
Ou melhor ainda:
export interface LocationModuleInterface {
readonly name: string;
getRecreationOptions(): ModuleRecreationOptions;
dontForgetToHaveStaticMethodForRecreation();
}
Se você estiver usando o objeto de classe polimorficamente, que é a única razão para ele implementar uma interface, não importa se as interfaces que especificam os requisitos para o lado estático da classe são referenciadas sintaticamente pela própria classe porque será verificado no site de uso e emitirá um erro de tempo de design se a(s) interface(s) necessária(s) não for(em) implementada(s).
@malina-kirn
Veja meu primeiro exemplo de 'digitação de pato'.
Esse problema não tem relação com o uso ou não da tipagem de pato. TypeScript é do tipo pato.
@aluanhaddad Sua implicação não está correta. Eu posso entender seu ponto de que a função primária implements ISomeInterface
é para usar o objeto de classe polimorficamente. No entanto, pode importar se a classe referencia a(s) interface(s) que descrevem a forma estática do objeto de classe.
Você implica que a inferência de tipo implícita e os erros do site de uso abrangem todos os casos de uso.
Considere este exemplo:
interface IComponent<TProps> {
render(): JSX.Element;
}
type ComponentStaticDisplayName = 'One' | 'Two' | 'Three' | 'Four';
interface IComponentStatic<TProps> {
defaultProps?: Partial<TProps>;
displayName: ComponentStaticDisplayName;
hasBeenValidated: 'Yes' | 'No';
isFlammable: 'Yes' | 'No';
isRadioactive: 'Yes' | 'No';
willGoBoom: 'Yes' | 'No';
}
interface IComponentClass<TProps> extends IComponentStatic<TProps> {
new (): IComponent<TProps> & IComponentStatic<TProps>;
}
function validateComponentStaticAtRuntime<TProps>(ComponentStatic: IComponentStatic<TProps>, values: any): void {
if(ComponentStatic.isFlammable === 'Yes') {
// something
} else if(ComponentStatic.isFlammable === 'No') {
// something else
}
}
// This works, we've explicitly described the object using an interface
const componentStaticInstance: IComponentStatic<any> = {
displayName: 'One',
hasBeenValidated: 'No',
isFlammable: 'Yes',
isRadioactive: 'No',
willGoBoom: 'Yes'
};
// Also works
validateComponentStaticAtRuntime(componentStaticInstance, {});
class ExampleComponent1 implements IComponent<any> {
public render(): JSX.Element { return null; }
public static displayName = 'One'; // inferred as type string
public static hasBeenValidated = 'No'; // ditto ...
public static isFlammable = 'Yes';
public static isRadioactive = 'No';
public static willGoBoom = 'Yes';
}
// Error: " ... not assignable to type IComponentStatic<..> ... "
validateComponentStaticAtRuntime(ExampleComponent1, {});
class ExampleComponent2 implements IComponent<any> {
public render(): JSX.Element { return null; }
public static displayName = 'One';
public static hasBeenValidated = 'No';
public static isFlammable = 'Yes';
public static isRadioactive = 'No';
public static willGoBoom = 'Yes';
}
// Error: " ... not assignable to type IComponentStatic<..> ... "
validateComponentStaticAtRuntime(ExampleComponent2, {});
No exemplo acima, é necessária uma declaração de tipo explícita; não descrever a forma do objeto de classe (lado estático) resulta em um erro de tempo de design no site de uso, mesmo que os valores reais estejam em conformidade com a forma esperada.
Além disso, a falta de uma maneira de referenciar a(s) interface(s) descrevendo o lado estático, nos deixa com a única opção de declarar explicitamente o tipo em cada membro individual; e depois repetir isso em todas as aulas.
Com base no meu post anterior, sinto que o modificador static
nas interfaces é uma solução muito boa para alguns casos de uso que precisam ser resolvidos. Peço desculpas pelo tamanho deste comentário, mas há muito que quero ilustrar.
1) Há momentos em que precisamos descrever explicitamente a forma do lado estático de um objeto de classe, como no meu exemplo acima.
2) Há momentos em que a verificação de forma no site de definição é altamente desejável, e momentos como no exemplo de @OliverJAsh em que o site de uso está em código externo que não será verificado e você precisa verificar a forma no site de definição .
Em relação ao número 2, muitos posts que li sugerem a verificação de forma, pois o site de uso é mais que suficiente. Mas nos casos em que o site de uso está em um módulo de galáxia muito, muito distante... ou quando o site de uso está em um local externo que não será verificado, isso obviamente não é o caso.
Outros posts sugerem #workarounds
para verificar a forma no site de definição. Embora essas temidas soluções alternativas permitam que você verifique a forma no local de definição (em alguns casos), existem problemas:
#workaroundsToMakeTheWorkaroundsWork
não é divertido... ou produtivo... mas principalmente não é divertido.Aqui está um exemplo da solução alternativa que vi recentemente para forçar a verificação de forma no site de definição...
// (at least) two separate interfaces
interface ISomeComponent { ... }
interface ISomeComponentClass { ... }
// confusing workaround with extra work to use, not very intuitive, etc
export const SomeComponent: ISomeComponentClass =
class SomeComponent extends Component<IProps, IState> implements ISomeComponent {
...
}
Agora tente usar essa solução alternativa com uma classe abstrata lançada
interface ISomeComponent { ... }
// need to separate the static type from the class type, because the abstract class won't satisfy
// the shape check for new(): ...
interface ISomeComponentStatic { ... }
interface ISomeComponentClass {
new (): ISomeComponent & ISomeComponentStatic;
}
// I AM ERROR. ... cannot find name abstract
export const SomeComponentBase: ISomeComponentStatic =
abstract class SomeComponentBase extends Component<IProps, IState> implements ISomeComponent {
...
}
export const SomeComponent: ISomeComponentClass =
class extends SomeComponentBase { ... }
Vamos, acho que vamos contornar isso também... suspiro
...
abstract class SomeComponentBaseClass extends Component<IProps, IState> implements ISomeComponent {
...
}
// Not as nice since we're now below the actual definition site and it's a little more verbose
// but hey at least were in the same module and we can check the shape if the use site is in
// external code...
// We now have to decide if we'll use this work around for all classes or only the abstract ones
// and instead mix and match workaround syntax
export const SomeComponentBase: ISomeComponentStatic = SomeComponentBaseClass;
Mas espere, há mais! Vamos ver o que acontece se estivermos usando genéricos ....
interface IComponent<TProps extends A> { ... }
interface IComponentStatic<TProps extends A> { ... }
interface IComponentClass<TProps extends A, TOptions extends B> extends IComponentStatic<TProps> {
new (options: TOptions): IComponent<TProps> & IComponentStatic<TProps>;
}
abstract class SomeComponentBaseClass<TProps extends A, TOptions extends B> extends Component<TProps, IState> implements IComponent<TProps> {
...
}
// Ruh Roh Raggy: "Generic type .... requires 2 type argument(s) ..."
export const SomeComponentBase: IComponentStatic = SomeComponentBaseClass;
// ".... requires 1 type argument(s) ... " OH NO, We need a different workaround
export const SomeComponent: IComponentStatic =
class extends SomeComponentBase<TProps extends A, ISomeComponentOptions> {
...
}
Neste ponto, se você tiver um site de uso dentro do código que será verificado pelo texto datilografado, provavelmente deve apenas morder a bala e confiar nele, mesmo que esteja muito longe.
Se você usar o site externo, convença a comunidade/mantenedores a aceitar o modificador static
para membros da interface. Enquanto isso, você precisará de outra solução alternativa. Não está no site de definição, mas acredito que você possa usar uma versão modificada de uma solução alternativa descrita em #8328 para conseguir isso. Veja a solução alternativa no comentário de @mhegazy em 16 de maio de 2016, cortesia de @RyanCavanaugh
Perdoe-me se estiver faltando pontos-chave. Minha compreensão da hesitação em dar suporte a static
para membros de interface é principalmente uma aversão ao uso da mesma palavra-chave interface + implements para descrever a forma do construtor e a forma da instância.
Deixe-me começar a seguinte analogia dizendo que eu amo o que o typescript oferece e aprecio muito aqueles que o desenvolveram e a comunidade que coloca muito pensamento e esforço para lidar com a infinidade de solicitações de recursos; fazendo o seu melhor para implementar os recursos que são um bom ajuste.
Mas, a hesitação neste caso me parece um desejo de preservar a 'pureza conceitual' à custa da usabilidade. Mais uma vez, desculpe se esta analogia está fora da base:
Isso lembrou de mudar de Java
para C#
. Quando vi pela primeira vez o código C#
fazendo isso
C#
var something = "something";
if(something == "something") {
...
}
os alarmes começaram a soar na minha cabeça, o mundo estava acabando, eles precisavam estar usando "something".Equals(something)
, etc
==
é para referência igual! Você tem um .Equals(...)
separado para comparação de strings...
e a string literal precisa vir primeiro para que você não receba uma referência nula chamando .Equals(...) em uma variável nula...
e... e... hiperventilando
E então, depois de algumas semanas usando C#
, percebi o quanto era mais fácil usar por causa desse comportamento. Mesmo que isso significasse desistir da distinção clara entre os dois, faz uma melhoria tão dramática na usabilidade que vale a pena.
É assim que me sinto sobre o modificador static
para membros da interface. Isso nos permitirá continuar descrevendo a forma da instância como já fazemos, nos permitirá descrever a forma da função construtora, o que só podemos fazer com soluções alternativas ruins caso a caso, e nos permitirá fazer as duas coisas de maneira relativamente limpa e fácil. caminho. Uma grande melhoria na usabilidade, IMO.
Eu vou pesar em abstract static
no próximo comentário....
Interfaces sempre requerem reespecificação de tipos de membros em classes de implementação. (a inferência de assinaturas de membros em classes de implementação _explicitamente_ é um recurso separado, ainda não suportado).
O motivo pelo qual você está recebendo erros não está relacionado a esta proposta. Para contornar isso, você precisa usar o modificador readonly
na implementação de membros de classe, estáticos ou não, que precisam ser de tipos literais; caso contrário, string
será inferido.
Novamente a palavra-chave implements
é apenas uma especificação de intenção, ela não influencia a compatibilidade estrutural dos tipos, nem introduz tipagem nominal. Isso não quer dizer que não possa ser útil, mas não altera os tipos.
Então abstract static
... Não vale a pena. Muitos problemas.
Você precisaria de uma sintaxe nova e complexa para declarar o que já será verificado no site de uso implicitamente (se o site de uso for verificado pelo compilador typescript).
Se o site de uso for externo, então o que você realmente precisa são static
membros de uma interface... Desculpe terminar de conectar... e boa noite!
@aluanhaddad Verdadeiro, mas mesmo que a inferência de assinaturas de membros em classes de implementação explícita fosse um recurso suportado, não temos maneiras limpas de declarar as assinaturas de membros para o lado estático da classe.
O ponto que eu estava tentando expressar era que não temos maneiras de explicitly
declarar a estrutura esperada do lado estático da classe e há casos em que isso importa (ou importará para oferecer suporte a recursos)
Principalmente eu queria refutar esta parte do seu comentário "não importa se a interface (s) que especifica os requisitos para o lado estático da classe é referenciada sintaticamente".
Eu estava tentando usar a inferência de tipo como um exemplo de por que isso importaria com suporte futuro.
Ao referenciar sintaticamente a interface aqui let something: ISomethingStatic = { isFlammable: 'Ys', isFlammable2: 'No' ... isFlammable100: 'No' }
não precisamos declarar explicitamente o tipo como 'Sim' | 'Não' 100 vezes separadas.
Para obter a mesma inferência de tipo para o lado estático de uma classe (no futuro), precisaremos de alguma maneira de referenciar sintaticamente a(s) interface(s).
Depois de ler seu último comentário, acho que não foi um exemplo tão claro quanto eu esperava. Talvez um exemplo melhor seja quando o site de uso para o lado estático da classe está em um código externo que não será verificado pelo compilador typescript. Nesse caso, atualmente temos que contar com soluções alternativas que essencialmente criam um site de uso artificial, fornecem usabilidade/legibilidade ruim e não funcionam em muitos casos.
À custa de alguma verbosidade, você pode alcançar um nível decente de segurança de tipo:
type HasType<T, Q extends T> = Q;
interface IStatic<X, Y> {
doWork: (input: X) => Y
}
type A_implments_IStatic<X, Y> = HasType<IStatic<X, Y>, typeof A> // OK
type A_implments_IStatic2<X> = HasType<IStatic<X, number>, typeof A> // OK
type A_implments_IStatic3<X> = HasType<IStatic<number, X>, typeof A> // OK
class A<X, Y> {
static doWork<T, U>(_input: T): U {
return null!;
}
}
type B_implments_IStatic = HasType<IStatic<number, string>, typeof B> // Error as expected
class B {
static doWork(n: number) {
return n + n;
}
}
Concordo que isso deve ser permitido. O seguinte caso de uso simples se beneficiaria de métodos estáticos abstratos:
export abstract class Component {
public abstract static READ_FROM(buffer: ByteBuffer): Component;
}
// I want to force my DeckComponent class to implement it's own static ReadFrom method
export class DeckComponent extends Component {
public cardIds: number[];
constructor(cardIds: number[]) {
this.cardIds = cardIds;
}
public static READ_FROM(buffer: ByteBuffer): DeckComponent {
const cardIds: number[] = [...];
return new DeckComponent(cardIds);
}
}
@RyanCavanaugh Acho que ninguém mencionou isso exatamente ainda (embora eu possa ter perdido na minha leitura rápida), mas em resposta a:
Uma questão importante que tivemos ao considerar isso: Quem tem permissão para chamar um método estático abstrato? Presumivelmente, você não pode invocar AbstractParentClass.getSomeClassDependentValue diretamente. Mas você pode invocar o método em uma expressão do tipo AbstractParentClass? Se sim, por que isso deveria ser permitido? Se não, qual é o uso do recurso?
Isso pode ser resolvido com a implementação de parte do #3841. Lendo essa questão, a principal objeção levantada parece ser que os tipos de funções construtoras de classes derivadas geralmente não são compatíveis com as funções construtoras de suas classes base. No entanto, a mesma preocupação não parece se aplicar a nenhum outro método ou campo estáticos porque o TypeScript já está verificando se os estáticos substituídos são compatíveis com seus tipos na classe base.
Então, o que eu proponho é dar T.constructor
o tipo Function & {{ statics on T }}
. Isso permitiria que classes abstratas que declaram um campo abstract static foo
o acessassem com segurança via this.constructor.foo
sem causar problemas com incompatibilidades de construtor.
E mesmo se a adição automática de estática a T.constructor
não for implementada, ainda poderemos usar as propriedades abstract static
na classe base declarando manualmente "constructor": typeof AbstractParentClass
.
Acho que muitas pessoas esperam que o exemplo @patryk-zielinski93 funcione. Em vez disso, devemos usar soluções alternativas contra-intuitivas, detalhadas e enigmáticas. Como já temos classes 'sintaxe açucaradas' e membros estáticos, por que não podemos ter esse açúcar no sistema de tipos?
Aqui está minha dor:
abstract class Shape {
className() {
return (<typeof Shape>this.constructor).className;
}
abstract static readonly className: string; // How to achieve it?
}
class Polygon extends Shape {
static readonly className = 'Polygon';
}
class Triangle extends Polygon {
static readonly className = 'Triangle';
}
Talvez pudéssemos introduzir/em paralelo uma cláusula static implements
? por exemplo
interface Foo {
bar(): number;
}
class Baz static implements Foo {
public static bar() {
return 4;
}
}
Isso tem a vantagem de ser compatível com versões anteriores com a declaração de interfaces separadas para membros estáticos e de instância, o que parece ser a solução alternativa atual de escolha, se eu estiver lendo este thread corretamente. É também um recurso sutilmente diferente que eu pude ver sendo útil por si só, mas isso não vem ao caso.
( statically implements
seria melhor, mas isso introduz uma nova palavra-chave. Discutível se vale a pena. Mas pode ser contextual.)
Por mais que eu tentasse entender os argumentos dos céticos em relação à proposta do OP, falhei.
Apenas uma pessoa (https://github.com/Microsoft/TypeScript/issues/14600#issuecomment-379645122) respondeu a você @RyanCavanaugh . Reescrevi o código do OP para torná-lo um pouco mais limpo e também para ilustrar sua pergunta e minhas respostas:
abstract class Base {
abstract static foo() // ref#1
}
class Child1 extends Base {
static foo() {...} // ref#2
}
class Child2 extends Base {
static foo() {...}
}
Quem tem permissão para chamar um método estático abstrato? Presumivelmente, você não pode invocar
Base.foo
diretamente
Se você quer dizer ref#1, então sim, nunca deve ser chamado, porque bem, é abstrato e nem tem um corpo.
Você só tem permissão para chamar ref#2.
Mas você pode invocar o método em uma expressão do tipo Base?
Se sim, por que isso deveria ser permitido?
function bar(baz:Base) { // "expression of type Base"
baz.foo() // ref#3
}
function bar2(baz: typeof Base) { // expression of type Base.constructor
baz.foo() // ref#4
}
ref#3 é um erro. Não, você não pode invocar "foo" lá, porque baz
deveria ser uma __instance__ de Child1 ou Child2, e as instâncias não possuem métodos estáticos
ref#4 está (deveria estar) correto. Você pode (deve ser capaz de) invocar o método estático foo lá, porque baz
deveria ser o construtor de Child1 ou Child2, que estendem Base e, portanto, devem ter implementado foo().
bar2(Child1) // ok
bar2(Child2) // ok
Você pode imaginar esta situação:
bar2(Base) // ok, but ref#4 should be red-highlighted with compile error e.g. "Cannot call abstract method."
// Don't know if it's possible to implement in compiler, though.
// If not, compiler simply should not raise any error and let JS to do it in runtime (Base.foo is not a function).
Qual é o uso do recurso?
Veja ref#4 O uso é quando não sabemos qual das classes filhas recebemos como argumento (pode ser Child1 ou Child2), mas queremos chamar seu método estático e ter certeza de que esse método existe.
Estou reescrevendo o framework node.js certo, chamado AdonisJS. Foi escrito em JS puro, então estou transformando para TS. Não posso mudar como o código funciona, estou apenas adicionando tipos. A falta desses recursos me deixa muito triste.
ps Neste comentário, por motivos de simplicidade, escrevi apenas sobre classes abstratas e não mencionei interfaces. Mas tudo o que escrevi é aplicável a interfaces. Você apenas substitui a classe abstrata pela interface e tudo estará correto. Existe a possibilidade, quando a equipe TS por algum motivo (não sei porque) não queira implementar abstract static
em abstract class
, porém seria bom implementar apenas static
palavra em interfaces, isso nos deixaria felizes o suficiente, eu acho.
pps Editei nomes de classes e métodos em suas perguntas para cumpri-los com meu código.
Alguns pensamentos, baseados nos principais argumentos de outros céticos:
"Não, isso não deve ser implementado, mas você já pode fazer assim *escreve 15 vezes mais linhas de código*".
É para isso que o açúcar sintático é feito. Vamos adicionar um pouco (açúcar) ao TS. Mas não, é melhor torturarmos os cérebros dos desenvolvedores que tentam quebrar os decoradores e dezenas de genéricos em vez de adicionar uma simples palavra static
às interfaces. Por que não considerar esse static
como mais um açúcar para facilitar nossas vidas? Da mesma forma que o ES6 adiciona class
(que se torna onipresente hoje em dia). Mas não, vamos ser nerds e fazer as coisas do jeito antigo e "certo".
"Existem duas interfaces para classes js: para construtor e instância".
Ok, por que não nos dar uma maneira de fazer interface para o construtor então? No entanto, simplesmente adicionar essa palavra static
é muito mais fácil e intuitivo.
E a respeito disso:
Segurando isso até ouvirmos mais comentários sobre isso.
Mais de um ano se passou e muitos comentários foram fornecidos, por quanto tempo essa questão será adiada? Alguém pode responder, por favor.
Este post pode parecer meio duro... Mas não, é um pedido do cara que ama TS e ao mesmo tempo não consegue encontrar uma razão sólida para irmos com hacks feios, ao converter nossa antiga base de código JS para TS. Além disso, muuuuito obrigado à equipe TS. TS é simplesmente maravilhoso, mudou minha vida e eu gosto mais do que nunca de codificação e meu trabalho... Mas esse problema está envenenando minha experiência.
Solução possível?
export type Constructor<T> = new (...args: any[]) => T;
declare global {
interface Function extends StaticInterface {
}
}
export interface StaticInterface {
builder: (this: Constructor<MyClass>) => MyClass
}
// TODO add decorator that adds "builder" implementation
export class MyClass {
name = "Ayyy"
}
export class OtherClass {
id = "Yaaa"
}
MyClass.builder() // ok
OtherClass.builder() // error
```` datilografado
interface Aplicativo estende Aplicar {estático de (a: A): Aplicativo ; }
const de = >(c: C, a: A): new C => c.of(a); ````
Serializable
$#$ com método deserialize
estático```` datilografado
interface serializável {
static desserialize(s: string): Serializable;
serialize(): string;
}
````
```` datilografado
contrato de interface {
static create(x: number): Contrato;
static new(x: number): Contrato;
}
const factory1 = (c: Contrato estático): Contrato => c.create(Math.random());
const factory2 = (C: Contrato estático): Contrato => new C(Math.random());
const fabrica3 =
const c1 = fabrica1(ContractImpl); // Contract
const c2 = fábrica2(ContractImpl); // Contract
const c3 = fabrica3(ContractImpl); // ContractImpl
````
```` datilografado
interface serializável {
static desserialize(s: string): Serializable;
}
tipo Serializavel = {
static desserialize(s: string): Serializable;
};
classe abstrata serializável {
static abstract desserialize(s: string): Serializable;
}
````
typescript
interface Contract {
static new(): Contract;
}
Discutir:
Como as assinaturas de chamada estáticas podem ser expressas?
Se vamos expressá-los simplesmente adicionando o modificador static
antes da assinatura da chamada,
como vamos distingui-los do método de instância com o nome 'static'
?
Podemos usar a mesma solução alternativa para os métodos 'new'
-named?
Nesse caso, será definitivamente uma mudança de ruptura.
static
typescript
const deserialize = (Class: static Serializable, s: string): Serializable =>
Class.deserialize(s);
new
typescript
const deserialize = <C extends static Serializable>(Class: C, s: string): new C =>
Class.deserialize(s);
[[Static]]
A interface “estática” de um tipo será armazenada no slot interno [[Static]]
:
```` datilografado
// O tipo
interface serializável {
static desserialize(s: string): Serializable;
serializar: string;
}
// será representado internamente como
//interface serializável {
// [[Estático]]: {
// [[Instância]]: Serializavel; // Veja abaixo.
// desserialize(s: string): Serializable;
// };
// serialize(): string;
// }
````
Por padrão, digite never
.
[[Instance]]
A interface “Instância” do tipo será armazenada
no slot interno [[Instance]]
#$8$#$ do tipo de slot interno [[Static]]
.
Por padrão, digite never
.
static
Quando um tipo usado como um tipo de valor,
o slot interno [[Static]]
será descartado:
```` datilografado
declare const serializável: Serializable;
tipo T = tipo de serializável;
// { serialize(): string; }
````
Mas o tipo em si permanece mantendo o slot interno [[Static]]
:
typescript
type T = Serializable;
// {
// [[Static]]: {
// [[Instance]]: Serializable;
// deserialize(s: string): Serializable;
// };
// serialize(): string;
// }
Ambos os valores dos tipos com reconhecimento de estática são atribuíveis à estrutura idêntica
(exceto os [[Static]]
, claro) e vice-versa.
static
| Associatividade | Precedência |
| :-----------: | :--------------------------------: |
| Direito | IDK, mas igual ao de new
|
O operador de tipo static
retorna o tipo de slot interno [[Static]]
do tipo.
É um pouco semelhante ao operador de tipo typeof
,
mas seu argumento deve ser um tipo em vez de um valor.
typescript
type T = static Serializable;
// {
// [[Instance]]: Serializable;
// deserialize(s: string): Serializable;
// }
O operador do tipo typeof
também descarta o slot interno [[Instance]]
:
```` datilografado
declare const SerializableImpl: static Serializable;
tipo T = tipo de SerializableImpl;
// { desserialize(s: string): Serializable; }
````
Ambos os valores dos tipos com reconhecimento de instância são atribuíveis à estrutura idêntica
(exceto o slot interno [[Instance]]
, é claro) e vice-versa.
new
| Associatividade | Precedência |
| :-----------: | :-----------------------------------: |
| Direito | IDK, mas igual ao de static
|
Os operadores new
retornam o tipo de slot interno [[Instance]]
do tipo.
Ele efetivamente reverte o operador static
:
typescript
type T = new static Serializable;
// {
// [[Static]]: {
// [[Instance]]: Serializable;
// deserialize(s: string): Serializable;
// };
// serialize(): string;
// }
extends
/ implements
semânticaUma classe que implementa uma interface com slot interno [[Static]]
não padrão
DEVE ter um slot interno compatível com [[Static]]
.
As verificações de compatibilidade devem ser iguais (ou semelhantes)
verificações de compatibilidade regulares (instância).
```` datilografado
class SerializableImpl implementa Serializable {
static desserialize(s: string): SerializableImpl {
// A lógica de desserialização vai aqui.
}
// ...other static members
// constructor
// ...other members
serialize(): string {
//
}
}
````
infer
?Passei algumas horas lendo esse problema e outros problemas para uma solução alternativa para o meu problema, que seria resolvido pelo modificador estático ou interfaces para classes. Não consigo encontrar uma única solução alternativa que resolva meu problema.
O problema que tenho é que quero fazer uma modificação em uma classe gerada por código. Por exemplo, o que eu gostaria de fazer é:
import {Message} from "../protobuf";
declare module "../protobuf" {
interface Message {
static factory: (params: MessageParams) => Message
}
}
Message.factory = function(params: MessageParams) {
const message = new Message();
//... set up properties
return message;
}
export default Message;
Não consigo encontrar uma única solução alternativa que me permita fazer o equivalente a isso, com a versão atual do TS. Estou perdendo uma maneira de resolver este problema atualmente?
Parece relevante postar aqui como um caso de uso para o qual aparentemente não há solução alternativa e certamente nenhuma solução alternativa direta.
Se você quiser verificar a instância e o lado estático de uma classe em relação às interfaces, você pode fazer assim:
interface C1Instance {
// Instance properties ...
// Prototype properties ...
}
interface C2Instance extends C1Instance {
// Instance properties ...
// Prototype properties ...
}
interface C1Constructor {
new (): C1Instance;
// Static properties ...
}
interface C2Constructor {
new (): C2Instance;
// Static properties ...
}
type C1 = C1Instance;
let C1: C1Constructor = class {};
type C2 = C2Instance;
let C2: C2Constructor = class extends C1 {};
let c1: C1 = new C1();
let c2: C2 = new C2();
Muitos de nós estão desperdiçando muitas horas do nosso tempo e outros com isso. Por que não é uma coisa?!¿i!
Por que as respostas são todas grandes soluções alternativas para algo que deve ser legível, digerível e algo que é uma linha simples. Não há como eu querer que alguém tenha que descobrir o que meu código está tentando fazer com alguma solução hacky. Desculpe por desordenar ainda mais este tópico com algo que não é muito valioso, mas é uma grande dor e perda de tempo atualmente, então acho valioso para mim, os outros agora e aqueles que vêm mais tarde procurando uma resposta.
A coisa com qualquer linguagem, natural e artificial, que deve evoluir naturalmente para ser eficiente e atraente para ser usada. Eventualmente as pessoas decidiram usar reduções na linguagem, ("okay"=>"ok","going to" =>"gonna"), inventaram novas palavras ridículas, como "selfie" e "google", redefiniram a ortografia com l33tspeak e coisas, e até baniu algumas palavras, e, apesar de você querer usá-las ou não, todo mundo ainda entende o que elas significam, e alguns de nós as usam para realizar algumas tarefas específicas. E para nenhum deles pode haver uma boa razão, mas a eficiência de certas pessoas em certas tarefas, é tudo uma questão de número de pessoas, que realmente fazem uso delas. O volume desta conversa mostra claramente que muitas pessoas poderiam fazer uso deste static abstract
para quaisquer considerações que tenham. Eu vim aqui pelo mesmo motivo, porque eu queria implementar Serializable
então tentei todas as maneiras intuitivas (para mim) de fazer isso, e nenhuma delas funcionou. Confie em mim, a última coisa que eu procuraria é a explicação de por que não preciso desse recurso e deveria optar por outra coisa. Ano e meio, Jesus Cristo! Aposto que já existe um PR em algum lugar, com testes e tal. Por favor, faça isso acontecer, e se houver uma certa forma de uso que é desencorajada, temos um tslint para isso.
É possível acessar membros estáticos de classes filhas da classe base via this.constructor.staticMember
, então membros estáticos abstratos fazem sentido para mim.
class A {
f() {
console.log(this.constructor.x)
}
}
class B extends A {
static x = "b"
}
const b = new B
b.f() // logs "b"
A classe A deve ser capaz de especificar que requer um membro estático x
, porque o usa no método f
.
Qualquer notícia ?
Os recursos são realmente necessários 😄
1) static
funções em interfaces
2) Funções abstract static
em classes abstratas
embora eu odeie a ideia de ter interfaces estáticas, mas para todos os fins práticos, o seguinte deve ser suficiente hoje :
type MyClass = (new (text: string) => MyInterface) & { myStaticMethod(): string; }
que pode ser usado como:
const MyClass: MyClass = class implements MyInterface {
constructor(text: string) {}
static myStaticMethod(): string { return ''; }
}
ATUALIZAR:
mais detalhes sobre a ideia e um exemplo ao vivo:
// dynamic part
interface MyInterface {
data: number[];
}
// static part
interface MyStaticInterface {
myStaticMethod(): string;
}
// type of a class that implements both static and dynamic interfaces
type MyClass = (new (data: number[]) => MyInterface) & MyStaticInterface;
// way to make sure that given class implements both
// static and dynamic interface
const MyClass: MyClass = class MyClass implements MyInterface {
constructor(public data: number[]) {}
static myStaticMethod(): string { return ''; }
}
// works just like a real class
const myInstance = new MyClass([]); // <-- works!
MyClass.myStaticMethod(); // <-- works!
// example of catching errors: bad static part
/*
type 'typeof MyBadClass1' is not assignable to type 'MyClass'.
Type 'typeof MyBadClass1' is not assignable to type 'MyStaticInterface'.
Property 'myStaticMethod' is missing in type 'typeof MyBadClass1'.
*/
const MyBadClass1: MyClass = class implements MyInterface {
constructor(public data: number[]) {}
static myNewStaticMethod(): string { return ''; }
}
// example of catching errors: bad dynamic part
/*
Type 'typeof MyBadClass2' is not assignable to type 'MyClass'.
Type 'typeof MyBadClass2' is not assignable to type 'new (data: number[]) => MyInterface'.
Type 'MyBadClass2' is not assignable to type 'MyInterface'.
Property 'data' is missing in type 'MyBadClass2'.
*/
const MyBadClass2: MyClass = class implements MyInterface {
constructor(public values: number[]) {}
static myStaticMethod(): string { return ''; }
}
@aleksey-bykov isso pode não ser culpa do Typescript, mas não consegui esses decoradores de componentes Angular funcionando e seu compilador AoT.
@aleksey-bykov isso é inteligente, mas ainda não funciona para estática abstrata. Se você tiver quaisquer subclasses de MyClass
, elas não serão aplicadas com verificação de tipo. Também é pior se você tiver genéricos envolvidos.
// no errors
class Thing extends MyClass {
}
Eu realmente espero que a equipe do TypeScript reconsidere sua posição sobre isso, porque construir bibliotecas de usuário final que requerem atributos estáticos não tem nenhuma implementação razoável. Devemos ser capazes de ter um contrato que exija que implementadores de interface/extensores de classe abstrata tenham estática.
@bbugh eu questiono a própria existência do problema que está sendo discutido aqui, por que você precisaria de todos esses problemas com métodos herdados abstratos estáticos se o mesmo pode ser feito por meio de instâncias de classes regulares?
class MyAbstractStaticClass {
abstract static myStaticMethod(): void; // <-- wish we could
}
class MyStaticClass extends MyAbstractStaticClass {
static myStaticMethod(): void {
console.log('hi');
}
}
MyStaticClass.myStaticMethod(); // <-- would be great
vs
class MyAbstractNonStaticClass {
abstract myAbstractNonStaticMethod(): void;
}
class MyNonStaticClass extends MyAbstractNonStaticClass {
myNonStaticMethod(): void {
console.log('hi again');
}
}
new MyNonStaticClass().myNonStaticMethod(); // <-- works today
@aleksey-bykov Existem muitas razões. Por exemplo ( de @patryk-zielinski93):
abstract class Serializable {
abstract serialize (): Object;
abstract static deserialize (Object): Serializable;
}
Eu quero forçar a implementação do método deserialize estático nas subclasses de Serializable.
Existe alguma solução alternativa para implementar esse comportamento?
EDIT: Eu sei que você também pode usar deserialize
como construtor, mas as classes só podem ter 1 construtor, o que torna os métodos de fábrica necessários. As pessoas querem uma maneira de exigir métodos de fábrica nas interfaces, o que faz todo o sentido.
simplesmente leve a lógica de deserialização para uma classe separada, porque não há benefício em ter um método de desserialização estático anexado à própria classe que está sendo desserializada
class Deserializer {
deserializeThis(...): Xxx {}
deserializeThat(...): Yyy {}
}
qual é o problema?
@aleksey-bykov aula separada não parece tão bonita
@aleksey-bykov, o problema é que há mais de 1 classe exigindo serialização, então sua abordagem está forçando a criação de um dicionário de serializáveis, que é um antipadrão uberclass, e cada modificação em qualquer um deles exigiria uma edição nesta uberclass, o que torna o suporte de código um incômodo. Embora ter uma interface serializável possa forçar uma implementação para qualquer tipo de objeto, e também suportar herança polimórfica, que são as principais razões pelas quais as pessoas a desejam. Por favor, não especule se há um benefício ou não, qualquer um deve poder ter opções e escolher o que é benéfico para seu próprio projeto.
@octaharon ter uma aula exclusivamente dedicada à desserialização é exatamente o oposto do que você disse, tem responsabilidade única porque a única vez que você muda é quando se preocupa com a desserialização
ao contrário, adicionar desserialização à própria classe, como você propôs, dá a ela uma responsabilidade adicional e um motivo adicional para ser alterada
por último, não tenho problemas com métodos estáticos, mas estou realmente curioso para ver um exemplo de um caso de uso prático para métodos estáticos abstratos e interfaces estáticas
@octaharon ter uma aula exclusivamente dedicada à desserialização é exatamente o oposto do que você disse, tem responsabilidade única porque a única vez que você muda é quando se preocupa com a desserialização
exceto que você precisa alterar o código em dois lugares em vez de um, pois sua (des)serialização depende da sua estrutura de tipo. Isso não conta para "responsabilidade única"
ao contrário, adicionar desserialização à própria classe, como você propôs, dá a ela uma responsabilidade adicional e um motivo adicional para ser alterada
Eu não entendo o que você está dizendo. Uma classe que seja responsável por sua própria (des)serialização é exatamente o que eu quero, e por favor, não me diga se está certo ou errado.
responsabilidade única não diz nada sobre quantos arquivos estão envolvidos, apenas diz que deve haver um único motivo
o que estou dizendo é que quando você adiciona uma nova classe você quer dizer algo diferente de apenas ser desserializado, então ela já tem uma razão de existir e responsabilidade atribuída a ela; então você adiciona outra responsabilidade de poder desserializar a si mesmo, isso nos dá 2 responsabilidades, isso viola o SOLID, se você continuar adicionando mais coisas como renderizar, imprimir, copiar, transferir, criptografar, etc, você obterá uma classe deus
e perdoe-me dizer banalidades, mas perguntas (e respostas) sobre benefícios e casos de uso práticos é o que impulsiona esta proposta de recurso a ser implementada
SOLID não foi enviado por Deus Todo-Poderoso em um tablet e, mesmo que fosse, há um grau de liberdade em sua interpretação. Você pode ser tão idealista quanto desejar, mas faça-me um favor: não espalhe suas crenças pela comunidade. Todo mundo tem todo o direito de usar qualquer ferramenta da maneira que quiser e quebrar todas as regras possíveis que conhece (você não pode culpar uma faca por um assassinato). O que define a qualidade de uma ferramenta é um equilíbrio entre a demanda por determinados recursos e a oferta deles. E esse ramo mostra o volume da demanda. Se você não precisa desse recurso - não o use. Eu faço. E um monte de gente aqui precisa disso também, e _nós_ temos um caso de uso prático, enquanto você está dizendo que deveríamos desconsiderá-lo, na verdade. É apenas sobre o que é mais importante para os mantenedores - os princípios sagrados (de qualquer forma que eles entendam), ou a comunidade.
cara, qualquer boa proposta apresenta casos de uso, esta não
então eu apenas expressei alguma curiosidade e fiz uma pergunta, já que a proposta não diz, por que vocês podem precisar
tudo se resumia ao típico: justa causa, não se atreva
pessoalmente, eu não me importo com sólido ou oop, eu acabei de crescer muito tempo atrás, você trouxe isso jogando o argumento do "antipadrão uberclass" e depois fez o backup para "um grau de liberdade para sua interpretação"
a única razão prática mencionada em toda essa discussão é esta: https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -308362119
Um caso de uso muito comum com o qual isso ajudará são os componentes React, que são classes com propriedades estáticas, como displayName, propTypes e defaultProps.
e algumas postagens semelhantes https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -345496014
mas está coberto por (new (...) => MyClass) & MyStaticInterface
esse é o caso de uso exato e uma razão pela qual estou aqui. Você vê o número de votos? Por que você acha que cabe a você decidir pessoalmente o que é practical
e o que não é? Practical
é o que pode ser colocado em practice
, e (no momento em que escrevo) 83 pessoas achariam esse recurso muito prático. Por favor, respeite os outros e leia o tópico completo antes de começar a tirar as frases de um contexto e exibir várias palavras-chave. O que quer que você tenha superado, isso definitivamente não é o seu ego.
é senso comum que as coisas práticas são as que resolvem os problemas, as não práticas são as que aguçam o seu senso de beleza, eu respeito os outros mas com todo esse respeito a pergunta (principalmente retórica agora) ainda vale: que problema esta proposta pretende resolva dado (new (...) => MyClass) & MyStaticInterface
para https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -308362119 e https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -289084844 justa causa
por favor não responda
Pela mesma razão que às vezes uso declarações type
para reduzir anotações grandes, acho que uma palavra-chave como abstract static
seria muito mais legível do que uma construção relativamente mais difícil de digerir, como foi mencionada como existente exemplo.
Além disso, ainda não abordamos classes abstratas?
A solução para não usar classes abstratas não é a solução, na minha opinião. Isso é uma solução! Uma solução em torno de quê?
Acho que essa solicitação de recurso existe porque muitas pessoas, incluindo o solicitante, descobriram que um recurso esperado, como abstract static
ou static
nas interfaces, não estava presente.
Com base na solução oferecida, a palavra-chave static
precisa existir se houver uma solução alternativa para evitar seu uso? Acho que seria igualmente ridículo sugerir.
O problema aqui é que static
faz muito mais sentido.
Com base no interesse gerado, podemos ter uma discussão menos desdenhosa?
Houve alguma atualização sobre esta proposta? Algum argumento que valha a pena considerar que demonstre por que não devemos ter static abstract
e coisas do gênero?
Podemos ter mais sugestões que mostrem por que seria útil?
Talvez precisemos resumir as coisas e resumir o que foi discutido para que possamos encontrar uma solução.
Existem duas propostas, pelo que entendo:
interface ISerializable<T> {
static fromJson(json: string): T;
}
abstract class MyClass<T> implements ISerializable<T> {
abstract static fromJson(json: string): T;
}
class MyOtherClass extends MyClass<any> {
static fromJson(json: string) {
// unique implementation
}
}
Quanto à proposta um, há tecnicamente uma solução alternativa! O que não é ótimo, mas isso é algo pelo menos. Mas é uma solução alternativa.
Você pode dividir suas interfaces em duas e reescrever sua classe como
interface StaticInterface {
new(...args) => MyClass;
fromJson(json): MyClass;
}
interface InstanceInterface {
toJson(): string;
}
const MyClass: StaticInterface = class implements InstanceInterface {
...
}
Na minha opinião, isso é muito trabalho extra e um pouco menos legível, e tem a desvantagem de reescrever suas classes de uma maneira engraçada que é simplesmente estranha e desvia da sintaxe que estamos usando.
Mas então, e a proposta 2? Não há nada que possa ser feito sobre isso, não é? Acho que isso merece ser abordado também!
Qual é o uso prático para um desses tipos - como um deles seria usado?
interface JsonSerializable {
toJSON(): string;
static fromJSON(serializedValue: string): JsonSerializable;
}
Já é possível dizer que um valor deve ser um objeto como { fromJSON(serializedValue: string): JsonSerializable; }
, então isso é apenas desejado para impor um padrão? Eu não vejo o benefício disso de uma perspectiva de verificação de tipo. Como uma observação lateral: neste caso, seria impor um padrão com o qual é difícil trabalhar - seria melhor mover o processo de serialização para classes ou funções de serializador separadas por muitos motivos que não entrarei aqui.
Além disso, por que algo assim está sendo feito?
class FirstChildClass extends AbstractParentClass {
public static getSomeClassDependentValue(): string {
return 'Some class-dependent value of class FirstChildClass';
}
}
Que tal usar um método de modelo ou padrão de estratégia? Isso funcionaria e seria mais flexível, certo?
No momento, sou contra esse recurso porque, para mim, parece adicionar complexidade desnecessária para descrever designs de classe com os quais é difícil trabalhar. Talvez haja algum benefício que estou perdendo?
existe um caso de uso válido para métodos React estáticos, é isso
@aleksey-bykov ah, ok. Para esses casos, pode ser melhor se adicionar um tipo na propriedade constructor
causar a verificação de tipo nesse cenário raro. Por exemplo:
interface Component {
constructor: ComponentConstructor;
}
interface ComponentConstructor {
displayName?: string;
}
class MyComponent implements Component {
static displayName = 5; // error
}
Isso me parece muito mais valioso. Ele não adiciona nenhuma complexidade adicional à linguagem e apenas adiciona mais trabalho para o verificador de tipos ao verificar se uma classe implementa uma interface corretamente.
A propósito, eu estava pensando que um caso de uso válido seria ao viajar de uma instância para o construtor e, finalmente, para um método ou propriedade estática com verificação de tipo, mas isso já é possível digitando a propriedade constructor
em um tipo e será resolvido para instâncias de classe em #3841.
eu tive uma ideia semelhante: https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -437071092
O padrão de serialização/desserialização descrito upthread não é "válido"?
@dsherret não "vê o benefício de uma perspectiva de verificação de tipo". O objetivo principal de fazer análise estática é detectar erros o mais cedo possível. Eu digito coisas para que, se uma assinatura de chamada precisar ser alterada, todos que a chamarem - ou, criticamente, todos os responsáveis pela implementação de métodos que usam a assinatura - atualizarão para a nova assinatura.
Suponha que eu tenha uma biblioteca que forneça um conjunto de classes irmãs com um método estático foo(x: number, y: boolean, z: string)
, e a expectativa é que os usuários escrevam uma classe de fábrica que receba várias dessas classes e chame o método para construir instâncias. (Talvez seja deserialize
ou clone
ou unpack
ou loadFromServer
, não importa.) O usuário também cria subclasses do mesmo pai (possivelmente abstrato) classe da biblioteca.
Agora, preciso alterar esse contrato para que o último parâmetro seja um objeto de opções. Passar um valor de string para o terceiro argumento é um erro irrecuperável que deve ser sinalizado em tempo de compilação. Qualquer classe de fábrica deve ser atualizada para passar um objeto ao chamar foo
, e as subclasses que implementam foo
devem alterar sua assinatura de chamada.
Quero garantir que os usuários que atualizarem para a nova versão da biblioteca captarão as alterações importantes em tempo de compilação. A biblioteca precisa exportar uma das soluções alternativas de interface estática acima (como type MyClass = (new (data: number[]) => MyInterface) & MyStaticInterface;
) e esperar que o consumidor a aplique em todos os lugares certos. Se eles esqueceram de decorar uma de suas implementações ou se não usaram um tipo de biblioteca exportada para descrever a assinatura de chamada de foo
em sua classe de fábrica, o compilador não pode dizer nada alterado e eles recebem erros de tempo de execução . Compare isso com uma implementação sensata de métodos abstract static
na classe pai -- sem necessidade de anotações especiais, sem carga no código de consumo, apenas funciona fora da caixa.
@thw0rted a maioria das bibliotecas não exigirá algum tipo de registro dessas classes e a verificação de tipos pode ocorrer nesse ponto? Por exemplo:
// in the library code...
class SomeLibraryContext {
register(classCtor: Function & { deserialize(serializedString: string): Component; }) {
// etc...
}
}
// then in the user's code
class MyComponent extends Comonent {
static deserialize(serializedString: string) {
return JSON.parse(serializedString) as Component;
}
}
const context = new SomeLibraryContext();
// type checking occurs here today. This ensures `MyComponent` has a static deserialize method
context.register(MyComponent);
Ou as instâncias dessas classes estão sendo usadas com a biblioteca e a biblioteca vai da instância ao construtor para obter os métodos estáticos? Nesse caso, é possível alterar o design da biblioteca para não exigir métodos estáticos.
Como uma nota lateral, como um implementador de uma interface, eu ficaria muito irritado se fosse forçado a escrever métodos estáticos porque é muito difícil injetar dependências em um método estático devido à falta de um construtor (nenhuma dependência ctor é possível). Também dificulta a troca da implementação da desserialização por outra coisa, dependendo da situação. Por exemplo, digamos que eu estava desserializando de diferentes fontes com diferentes mecanismos de serialização... agora eu tenho um método de desserialização estático que, por design, só pode ser implementado de uma maneira para uma implementação de uma classe. Para contornar isso, eu precisaria ter outra propriedade ou método estático global na classe para dizer ao método deserialize estático o que usar. Eu preferiria uma interface Serializer
separada que permitiria trocar facilmente o que usar e não acoplar a (des)serialização à classe que está sendo (des)serializada.
Entendo o que você quer dizer -- para usar um padrão de fábrica, eventualmente você precisa passar a classe de implementação para a fábrica, e a verificação estática ocorre nesse momento. Eu ainda acho que pode haver momentos em que você deseja fornecer uma classe compatível com a fábrica, mas não a usa no momento; nesse caso, uma restrição abstract static
detectaria problemas mais cedo e tornaria o significado mais claro.
Eu tenho outro exemplo que acho que não entra em conflito com sua preocupação com a "nota lateral". Eu tenho várias classes irmãs em um projeto de frontend, onde dou ao usuário uma escolha de qual "provedor" usar para um determinado recurso. Claro, eu poderia fazer um dicionário em algum lugar de name => Provider
e usar as chaves para determinar o que mostrar na lista de opções, mas a maneira como implementei agora é exigir um campo estático name
em cada implementação do Provedor.
Em algum momento, mudei isso para exigir shortName
e longName
(para exibição em diferentes contextos com diferentes quantidades de espaço de tela disponível). Teria sido muito mais simples alterar abstract static name: string;
para abstract static shortName: string;
etc, em vez de alterar o componente da lista de opções para ter um providerList: Type<Provider> & { shortName: string } & { longName: string }
. Ele transmite a intenção, no lugar certo (ou seja, no pai abstrato, não no componente de consumo) e é fácil de ler e alterar. Acho que podemos dizer que há uma solução alternativa, mas ainda acredito que seja objetivamente pior do que as mudanças propostas.
Recentemente, me deparei com esse problema, quando precisei usar o método estático em uma interface. Peço desculpas, se isso já foi abordado antes, pois não tenho tempo para ler essa quantidade de comentários.
Meu problema atual: tenho uma biblioteca .js privada, que quero usar no meu projeto TypeScript. Então eu fui em frente e comecei a escrever um arquivo .d.ts para aquela biblioteca, mas como a biblioteca usa métodos estáticos, eu não consegui terminar isso. Qual é a abordagem sugerida neste caso?
Obrigado por respostas.
@greeny https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -449610196
@greeny aqui está uma solução de trabalho: https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -437071092
especificamente esta parte:
type MyClass = (new (text: string) => MyInterface) & { myStaticMethod(): string; }
Outro caso de uso: código gerado / correção parcial de classe
import
sejam ignoradas na mesclagem final para que a inclusão 'shim' seja ignorada no código gerado final.interface SwaggerException
{
static isSwaggerException(obj: any): obj is SwaggerException;
}
Mas eu não posso fazer isso e tenho que realmente escrever uma classe - o que é bom, mas ainda parece errado. Eu só quero - como muitos outros disseram - dizer 'este é o contrato'
Só queria adicionar isso aqui, já que não vejo outras menções à geração de código - mas muitos comentários 'por que você precisaria disso' que ficam irritantes. Eu esperaria que uma porcentagem decente de pessoas 'precisando' desse recurso 'estático' em uma interface estivesse fazendo isso apenas para que pudessem se referir a itens de bibliotecas externas.
Também gostaria de ver arquivos d.ts
mais limpos - deve haver alguma sobreposição com esse recurso e esses arquivos. É difícil entender algo como JQueryStatic porque parece que é apenas um hack. Além disso, a realidade é que os arquivos d.ts
geralmente estão desatualizados e não são mantidos e você mesmo precisa declarar os calços.
(desculpe por mencionar jQuery)
Para o caso de serialização eu fiz algo assim.
export abstract class World {
protected constructor(json?: object) {
if (json) {
this.parseJson(json);
}
}
/**
* Apply data from a plain object to world. For example from a network request.
* <strong i="6">@param</strong> json Parsed json object
*/
abstract parseJson(json: object): void;
/**
* Parse instance to plane object. For example to send it through network.
*/
abstract toJson(): object;
}
Mas ainda seria muito mais fácil usar algo assim:
export abstract class World {
/**
* Create a world from plain object. For example from a network request.
* <strong i="10">@param</strong> json Parsed json object
*/
abstract static fromJson(json: object): World;
/**
* Parse instance to plane object. For example to send it through network.
*/
abstract toJson(): object;
}
Li muito esse tópico e ainda não entendo por que é bom e correto dizer não a esse padrão. Se você compartilha dessa opinião, você também diz que java e outras linguagens populares fazem isso errado. Ou eu estou errado?
Esta edição está aberta há dois anos e tem 79 comentários. Ele é rotulado como Awaiting More Feedback
. Você poderia dizer o que mais feedback você precisa para tomar uma decisão?
é realmente irritante que eu não possa descrever o método estático nem na interface nem na classe abstrata (somente declaração). É tão feio escrever soluções alternativas porque esse recurso ainda não foi implementado =(
Por exemplo, next.js usa uma função estática getInitialProps
para obter as propriedades da página antes de construir a página. Caso seja lançada, a página não é construída, mas sim a página de erro.
https://github.com/zeit/next.js/blob/master/packages/next/README.md#fetching -data-and-component-lifecycle
Mas, infelizmente, pistas que implementam esse método podem fornecer qualquer assinatura de tipo, mesmo que causem erros em tempo de execução, porque não podem ser verificados de tipo.
Eu acho que esse problema existe aqui há tanto tempo é porque o próprio JavaScript não é bom em coisas estáticas 🤔
A herança estática nunca deve existir em primeiro lugar. 🤔🤔
Quem quer chamar um método estático ou ler um campo estático? 🤔🤔🤔
Existe algum outro caso de uso?🤔🤔🤔🤔
Alguma atualização sobre isso?
Se for de alguma ajuda, o que eu fiz nos casos em que preciso digitar a interface estática para uma classe é usar um decorador para impor os membros estáticos na classe
O decorador é definido como:
export const statics = <T extends new (...args: Array<unknown>) => void>(): ((c: T) => void) => (_ctor: T): void => {};
Se eu tiver a interface de membro do construtor estático definida como
interface MyStaticType {
new (urn: string): MyAbstractClass;
isMember: boolean;
}
e invocado na classe que deve declarar estaticamente os membros em T como:
@statics<MyStaticType>()
class MyClassWithStaticMembers extends MyAbstractClass {
static isMember: boolean = true;
// ...
}
O exemplo mais frequente é bom:
interface JsonSerializable {
toJSON(): string;
static fromJSON(serializedValue: string): JsonSerializable;
}
Mas como dito em #13462 aqui :
As interfaces devem definir a funcionalidade que um objeto fornece. Essa funcionalidade deve ser substituível e intercambiável (é por isso que os métodos de interface são virtuais). A estática é um conceito paralelo ao comportamento dinâmico/métodos virtuais.
Concordo com o ponto de que as interfaces, no TypeScript, descrevem apenas uma instância de objeto e como usá-la. O problema é que uma instância de objeto não é uma definição de classe , e um símbolo static
pode existir apenas em uma definição de classe .
Assim, posso propor o seguinte, com todas as suas falhas:
Uma interface pode estar descrevendo um objeto ou uma classe . Digamos que uma interface de classe seja marcada com as palavras-chave class_interface
.
class_interface ISerDes {
serialize(): string;
static deserialize(str: string): ISerDes
}
As classes (e interfaces de classe) podem usar as palavras-chave statically implements
para declarar seus símbolos estáticos usando uma interface de objeto (as interfaces de classe não podem ser statically
implementadas).
As classes (e interfaces de classe) ainda usariam a palavra-chave implements
com uma interface de objeto ou uma interface de classe .
Uma interface de classe poderia, então, a mistura entre uma interface de objeto implementada estaticamente e uma interface implementada por instância. Assim, poderíamos obter o seguinte:
interface ISerializable{
serialize(): string;
}
interface IDeserializable{
deserialize(str: string): ISerializable
}
class_interface ISerDes implements ISerializable statically implements IDeserializable {}
Dessa forma, as interfaces poderiam manter seu significado, e class_interface
seria um novo tipo de símbolo de abstração dedicado a definições de classes.
Um pequeno caso de uso não crítico a mais:
Em Angular para compilação AOT, você não pode chamar funções em decoradores (como no decorador de módulo @NgModule
)
problema angular
Para o módulo do service worker, você precisa de algo assim:
ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production})
Nosso ambiente usando subclasses, estendendo a classe abstrata com valores padrão e propriedades abstratas para implementar. Exemplo de implementação semelhante
Portanto, o AOT não funciona porque construir uma classe é uma função e gera erros como: Function calls are not supported in decorators but ..
Para fazê-lo funcionar e manter o suporte ao autocompletion/compiler, é possível definir as mesmas propriedades em nível estático. Mas para 'implementar' propriedades, precisamos de interface com membros estáticos ou membros estáticos abstratos na classe abstrata. Ambos ainda não são possíveis.
com interface poderia funcionar assim:
// default.env.ts
interface ImplementThis {
static propToImplement: boolean;
}
class DefaultEnv {
public static production: boolean = false;
}
// my.env.ts
class Env extends DefaultEnv implements ImplementThis {
public static propToImplement: true;
}
export const environment = Env;
com estática abstrata poderia funcionar assim:
// default.env.ts
export abstract class AbstractDefaultEnv {
public static production: boolean = false;
public abstract static propToImplement: boolean;
}
// my.env.ts
class Env extends AbstractDefaultEnv {
public static propToImplement: true;
}
export const environment = Env;
existem soluções alternativas , mas todas elas são fracas :/
@DanielRosenwasser @RyanCavanaugh pede desculpas pelas menções, mas parece que essa sugestão de recurso - que tem muito apoio da comunidade e acho que seria bastante fácil de implementar - ficou profundamente enterrada na categoria Problemas. Algum de vocês tem algum comentário sobre esse recurso, e um PR seria bem-vindo?
Este problema não é uma duplicata do nº 1263? 😛
Eu sinto que métodos estáticos em interfaces não são tão intuitivos quanto ter métodos abstratos estáticos em classes abstratas.
Eu acho que é um pouco esboçado adicionar métodos estáticos a interfaces porque uma interface deve definir um objeto, não uma classe. Por outro lado, uma classe abstrata certamente deve ter permissão para ter métodos abstratos estáticos, uma vez que classes abstratas são usadas para definir subclasses. No que diz respeito à implementação disso, ela simplesmente precisaria ser verificada ao estender a classe abstrata (por exemplo class extends MyAbstractClass
), não ao usá-la como um tipo (por exemplo let myInstance: MyAbstractClass
).
Exemplo:
abstract class MyAbstractClass {
static abstract bar(): number;
}
class Foo extends MyAbstractClass {
static bar() {
return 42;
}
}
agora por necessidade eu uso isso
abstract class MultiWalletInterface {
static getInstance() {} // can't write a return type MultiWalletInterface
static deleteInstance() {}
/**
* Returns new random 12 words mnemonic seed phrase
*/
static generateMnemonic(): string {
return generateMnemonic();
}
}
isso é inconveniente!
Eu vim com um problema em que estou adicionando propriedades ao "Objeto", aqui está um exemplo de sandbox
interface Object {
getInstanceId: (object: any) => number;
}
Object.getInstanceId = () => 42;
const someObject = {};
Object.getInstanceId(someObject); // correct
someObject.getInstanceId({}); // should raise an error but does not
Qualquer instância de objeto agora é considerada como tendo a propriedade getInstanceId
enquanto apenas Object
deveria. Com uma propriedade estática, o problema teria sido resolvido.
Você deseja aumentar ObjectConstructor, não Object. Você está declarando um método de instância, quando na verdade deseja anexar um método ao próprio construtor. Eu acho que isso já é possível através da mesclagem de declarações:
````ts
declarar global {
interface ObjectConstructor {
ola(): string;
}
}
Object.hello();
````
@thw0rted Excelente! obrigado eu não estava ciente do ObjectConstructor
O ponto maior é que você está aumentando o tipo de construtor em vez do tipo de instância. Acabei de procurar a declaração Object
em lib.es5.d.ts
e descobri que é do tipo ObjectConstructor
.
É difícil para mim acreditar que esse problema ainda está por aí. Este é um recurso legitimamente útil, com vários casos de uso reais.
O ponto principal do TypeScript é ser capaz de garantir a segurança de tipo em nossa base de código, então, por que esse recurso ainda está "pendente de feedback" após dois anos de feedback?
Eu posso estar longe disso, mas algo como as metaclasses do Python seriam capazes de resolver esse problema de uma maneira nativa e sancionada (ou seja, não um hack ou solução alternativa) sem violar o paradigma do TypeScript (ou seja, manter tipos de TypeScript separados para a instância e a classe)?
Algo assim:
interface DeserializingClass<T> {
fromJson(serializedValue: string): T;
}
interface Serializable {
toJson(): string;
}
class Foo implements Serializable metaclass DeserializingClass<Foo> {
static fromJson(serializedValue: string): Foo {
// ...
}
toJson(): string {
// ...
}
}
// And an example of how this might be used:
function saveObject(Serializable obj): void {
const serialized: string = obj.toJson();
writeToDatabase(serialized);
}
function retrieveObject<T metaclass DeserializingClass<T>>(): T {
const serialized: string = getFromDatabase();
return T.fromJson(serialized);
}
const foo: Foo = new Foo();
saveObject(foo);
const bar: Foo = retrieveObject<Foo>();
Honestamente, a parte mais complicada dessa abordagem parece que seria criar uma palavra-chave significativa do TypeScript para metaclass
... staticimplements
, classimplements
, withstatic
, implementsstatic
... não tenho certeza.
Isso é um pouco como a proposta do @GerkinDev , mas sem o tipo separado de interface. Aqui, há um único conceito de interface, e eles podem ser usados para descrever a forma de uma instância ou de uma classe. Palavras-chave na definição da classe de implementação informariam ao compilador em que lado cada interface deve ser verificada.
Vamos retomar a discussão em #34516 e #33892, dependendo de qual recurso você está procurando
Comentários muito úteis
Eu quero forçar a implementação do método deserialize estático nas subclasses de Serializable.
Existe alguma solução alternativa para implementar esse comportamento?