Definindo métodos estáticos em interfaces com TypeScript
Tags: typescript, javascript, development
Published: 2023-10-20 22:25:47

---
title: Definindo métodos estáticos em interfaces com TypeScript
published: true
date: 2023-10-17 22:00:33 UTC
tags: typescript,javascript,development
canonical_url: https://blog.lsantos.dev/interfaces-estaticas-typescript/
cover_image: https://raw.githubusercontent.com/khaosdoctor/blog-assets/master/images/2023/10/articles-2.png
---

Ontem eu estava montando as aulas da [Formação TS](https://formacaots.com.br/?ref=blog.lsantos.dev) e me deparei com algo que é bastante interessante e que eu mesmo já quebrei a cabeça por anos (inclusive ontem).

Quando a gente fala em programação orientada a objetos uma das coisas mais difíceis de entender é o conceito de propriedades estáticas versus propriedades de instâncias, e isso é especialmente difícil quando tentamos tipar uma linguagem dinâmica em cima de uma tipagem estática.

Eu não vou entrar em detalhes sobre o que são métodos estáticos ou não estáticos neste artigo porque existem muitos outros conteúdos na Internet que você pode consumir que vão ser muito mais detalhados do que eu posso ser aqui.

Mas vale a pena dar aquela refrescada na memória.

## Métodos estáticos

Métodos estáticos, ou atributos estáticos, são atributos que existem em qualquer instância de uma classe, eles são definidos no nível de construtor, ou seja, a própria classe possui esses métodos e, portanto, todas as instâncias dessas classes também vão possuir.

Um método estático comum é quando estamos criando um objeto de domínio, ou uma entidade de banco de dados, por exemplo:

```
class Person {
  static fromObject (obj: Record<string, unknown>) {
    const instance = new Person()
    instance.prop = obj.prop
    return instance
  }

  toObject () {
    return {
     prop: this.prop
    }
}
```

O método `fromObject` existe em todas as classes, ele está acima de qualquer instância e, por isso, não pode usar a palavra chave `this`, porque o `this` [ainda não foi inicializado](https://twitter.com/_StaticVoid/status/1708966256487911727?ref=blog.lsantos.dev), e também porque você está em um contexto acima de qualquer instância que o `this` poderia se referir.

Nesse caso a gente está recebendo um objeto e criando uma nova instância da classe diretamente com ele. Para a gente executar esse código, ao invés de fazer algo padrão como:

```
const p = new Person()
p.fromObject(etc) // erro, a propriedade não existe na instância
```

A gente precisa chamar o método direto do construtor da classe:

```
const p = Person.fromObject(etc)
```

# O problema

Métodos estáticos são muito comuns em linguagens fortemente tipadas, porque você tem uma clara separação entre o momento estático da classe e o momento "dinâmico".

Mas o que acontece quando a gente precisa tipar uma linguagem dinâmica com uma tipagem estática?

No TypeScript a gente vai ter alguns erros quando tentarmos declarar, por exemplo, que uma classe tem métodos dinâmicos e estáticos e tentar descrever os dois em uma interface:

```
interface Serializable {
  fromObject (obj: Record<string, unknown>): Person
  toObject (): Record<string, unknown>
}

class Person implements Serializable 
// Class 'Person' incorrectly implements interface 'Serializable'.
// Property 'fromObject' is missing in type 'Person' but required in type
// 'Serializable'.
```

Isso acontece porque interfaces no TypeScript atuam no "[lado dinâmico](https://www.typescriptlang.org/docs/handbook/interfaces.html?ref=blog.lsantos.dev#difference-between-the-static-and-instance-sides-of-classes)" da classe, então é como se todas as interfaces fossem instâncias da classe em questão, mas não a classe em si.

Felizmente, o TypeScript tem uma forma de declarar uma classe como um construtor, as chamadas **Constructor Signatures** :

```
interface Serializable {
  new(...args: any[]): any
  fromObject (obj: Record<string, unknown>): Person
  toObject (): Record<string, unknown>
}
```

Agora vai, certo? Infelizmente não, porque mesmo que você implemente o método manualmente, a classe ainda dirá que você não implementou o método `fromObject`.

## O problema da reflexão estática

E o problema vai mais além, por exemplo, se quiséssemos fazer uma classe de banco de dados que usa o nome da entidade direto da classe para poder criar um arquivo, isso é feito através da propriedade `name` em qualquer classe, essa é uma propriedade estática que existe em todos os objetos instanciáveis:

```
interface Serializable {
  toObject (): any
}

class DB {
  constructor (entity: Serializable) {
    const path = entity.name // name não existe na propriedade
  }
}
```

Ok, então podemos substituir `entity.name` por `entity.constructor.name`, o que funciona, mas e quando a gente precisa criar uma nova entidade a partir de um objeto?

```
interface Serializable {
  toObject (): any
}

class DB {

  #entity: Serializable
  constructor (entity: Serializable) {
    const path = entity.constructor.name
    this.#entity = entity
  }

  readFromFile () {
    // lemos do arquivo aqui
    const object = 'conteúdo do arquivo como um objeto'
    return this.#entity.fromObject(object) // fromObject não existe
  }

}
```

Então estamos em uma escolha, ou a gente prioriza a instância ou a gente prioriza o construtor...

# A solução

Felizmente, temos uma solução para esse problema, ela não é muito bonita, mas existem algumas ideias no repositório do TypeScript (como [essa](https://github.com/microsoft/TypeScript/issues/33892?ref=blog.lsantos.dev) e [essa](https://github.com/microsoft/TypeScript/issues/34516?ref=blog.lsantos.dev)) que vem [desde 2017](https://github.com/microsoft/TypeScript/issues/13462?ref=blog.lsantos.dev) que estão analisando a possibilidade de adicionar definições estáticas à interfaces.

Porém, enquanto essa ideia não chega, o que temos é a definição de duas partes da interface, a parte estática e a parte da instância:

```
export interface SerializableStatic {
  new (...args: any[]): any
  fromObject(data: Record<string, unknown>): InstanceType<this>
}

export interface Serializable {
  id: string
  toJSON(): string
}
```

> É importante notar que o construtor em `new(...args: any[]): any` **precisa** ser tipado como `any` no retorno, caso contrário ela vira uma referência circular

Tendo essas duas partes da classe tipadas, podemos dizer que a classe só implementa a parte da instância:

```
class Person implements Serializable {
  // ...
}
```

E ai agora, podemos dizer que nosso banco de dados vai receber dois type parameters, um será a parte estática, que vamos chamar de `S` e outro vai ser a parte dinâmica (ou da instância) que vamos chamar de `I`, `S` sempre vai estender `SerializableStatic` e `I` sempre vai estender `Serializable` e, por padrão, vai ser o tipo de uma instância de `S`, que pode ser definido pelo type utility `InstanceType<S>`:

```
class Database<S extends SerializableStatic, I extends Serializable = InstanceType<S>> { }
```

Agora podemos ter as nossas propriedades normalmente, por exemplo:

```
class Database<S extends SerializableStatic, I extends Serializable = InstanceType<S>> {
  #dbPath: string
  #data: Map<string, I> = new Map()
  #entity: S

  constructor(entity: S) {
    this.#dbPath = resolve(dirname(import.meta.url), `.data/${entity.name.toLowerCase()}.json`)
    this.#entity = entity
    this.#initialize()
  }
}
```

E no nosso método `#initialize` vamos usar o método `fromObject` para poder ler diretamente do arquivo e transformar em uma instância de uma classe:

```
class Database<S extends SerializableStatic, I extends Serializable = InstanceType<S>> {
  #dbPath: string
  #data: Map<string, I> = new Map()
  #entity: S

  constructor(entity: S) {
    this.#dbPath = resolve(dirname(import.meta.url), `.data/${entity.name.toLowerCase()}.json`)
    this.#entity = entity
    this.#initialize()
  }

  #initialize() {
    if (existsSync(this.#dbPath)) {
      const data: [string, Record<string, unknown>][] = JSON.parse(readFileSync(this.#dbPath, 'utf-8'))
      for (const [key, value] of data) {
        this.#data.set(key, this.#entity.fromObject(value))
      }
      return
    }
    this.#updateFile()
  }
}
```

Enquanto podemos ter métodos como `get` e `getAll`, ou até `save` que recebem e retornam instâncias apenas.

```
get(id: string): I | undefined {
  return this.#data.get(id)
}

getAll(): I[] {
  return [...this.#data.values()]
}

save(entity: I): this {
  this.#data.set(entity.id, entity)
  return this.#updateFile()
}
```

Agora quando usarmos um banco de dados desse tipo como:

```
class Person implements Serializable {
  // código aqui
}

const db = new DB(Person)
const todas = db.getAll() // Person[]
const umaOuNenhuma = db.get(1) // Person | undefined
db.save(new Person()) // DB<Person>
```

> Se você curtiu esse conteúdo, vem aprender mais sobre TypeScript comigo lá na **Formação TS!** É só acessar [https://formacaots.com.br](https://formacaots.com.br/?ref=blog.lsantos.dev) e fazer parte dos mais de 250 alunos que já estão curtindo!