DEV Community

Cover image for Construindo uma Promise do zero
Lucas Santos
Lucas Santos

Posted on • Updated on

Construindo uma Promise do zero

Há um tempo atrás publiquei um artigo sobre como podemos entender Promises de uma vez por todas, se você ainda não leu, recomendo a leitura para que possamos continuar, mas vou dar um pequeno resumo do que conversamos nele.

Promises são estruturas que lidam com valores que podem ser obtidos futuramente em uma execução, por exemplo, uma requisição para um servidor externo, ou então uma leitura de um arquivo. O que poucos sabem é que, de fato, a Promise é um padrão de projeto que pode ser implementado utilizando orientação a objetos e a especificação descrita no PromisesA+.

Como a melhor forma de aprender é fazendo, vamos mergulhar na especificação das Promises e vamos implementar nossa própria Promise do zero!

Especificação

A especificação de Promises como conhecemos no JavaScript está no Promises/A+ (uma evolução do Promises/A), pois, antes da implementação nativa, algumas bibliotecas como o Q e o Bluebird já implementavam este padrão. Então resolveu-se criar uma especificação aberta onde as pessoas que implementavam este modelo pudessem escrever e debater estes assuntos com outros programadores. Esta especificação define, basicamente, como o método then deve funcionar, de forma que todas as Promises que conformem com ela devem funcionar da mesma maneira em qualquer lugar.

Terminologia

Vamos dar nome a algumas coisas, primeiramente vamos definir todos os termos que vamos utilizar na nossa promise, isto vem de uma tradução direta da especificação:

  • A Promise é um objeto com um método then cujo comportamento conforma com esta especificação
  • Um thenable é um objeto ou função que define um método then
  • Um valor é qualquer valor válido no JavaScript (incluindo undefined, um thenable ou até outra promise)
  • Uma exception é uma exceção padrão de desenvolvimento que é levantada a partir de um throw
  • A razão é o motivo pelo qual uma promise foi rejeitada (quando sofre uma exception)

Estado

A Promise é essencialmente uma máquina de estados. Ela pode estar em um de três possíveis estados:

  • Pendente: Neste estado ela pode ir para fulfilled ou rejected
  • Fulfilled (completa): Neste estado, a promise não pode transicionar para nenhum outro estado; Também deve possuir um valor que não deve ser alterável
  • Rejected (rejeitada): Neste estado, a promise não pode transicionar para nenhum outro estado; Também deve possuir uma razão que não deve ser alterável

Then

Todas as Promises precisam especificar um método then que vai ser o responsável por, de fato, avaliar a função e retornar o valor atual. Todo método then deve ter a seguinte assinatura:

promise.then(onFulfilled, onRejected)
Enter fullscreen mode Exit fullscreen mode

Onde, onFulfilled é uma função com a seguinte assinatura:

(value: T) => void
Enter fullscreen mode Exit fullscreen mode

E onRejected tem a mesma assinatura, porém com uma razão ao invés de um valor.

Além disso, o then precisa seguir uma série de regras para poder ser considerado conforme com a especificação. Não vou colocar todas elas aqui, mas vou incluir as mais importantes:

  • Tanto onFulfilled quanto onRejected são parâmetros opcionais para o then e devem ser ignorados se não forem funções
  • onFulfilled, quando aceito, deve ser chamado sempre após a promise ter sido resolvida, com o valor da promise como primeiro argumento. Além disso ele só pode ser chamado uma vez.
  • onRejected, quando aceito, deve ser chamado sempre após a promise ter sido rejeitada, com a razão da promise como primeiro argumento. Além disso ele só pode ser chamado uma vez.
  • then pode ser encadeado múltiplas vezes na mesma promise. Quando a promise é completada ou rejeitada, todos os handlers then devem ser executados em ordem.
  • then deve retornar outra promise

Implementação

Para começarmos a implementar nossa promise, vamos primeiro criar um arquivo chamado PromiseTypes.ts, vamos usar Typescript para poder fazer com que ela tenha alguns tipos que vão deixar muito mais simples o entendimento. Neste arquivo vamos colocar os tipos globais, que já sabemos que existem, como a função onFulfilled e onRejected, executores e tudo mais.

Vamos começar criando um enumerador com os estados possíveis de uma promise:

export enum PromiseStates {
  PENDING,
  FULFILLED,
  REJECTED
}
Enter fullscreen mode Exit fullscreen mode

Agora vamos criar os tipos de apoio e os tipos base da promise que vamos usar:

export type ResolveFunction = (value: any) => any
export type RejectFunction = (reason: any) => any
export type Thennable = { then: (value: any) => TypePromise }
export type ExecutorFunction = (resolve: ResolveFunction, reject: RejectFunction) => void
Enter fullscreen mode Exit fullscreen mode

Estamos chamando de executor a função que a Promise irá receber em seu construtor, ela que deverá conter o resolve e o reject. Da mesma forma, criamos um tipo para o thennable. Vamos também criar um outro tipo auxiliar só para que possamos deixar nosso código mais bonito, chamado nullable<T>, ele vai servir para podermos implementar elementos que podem ser nulos:

export type Nullable<T> = T | null
Enter fullscreen mode Exit fullscreen mode

Máquina de estados

Vamos começar criando um arquivo chamado TypePromise.ts, vamos chamar nossa classe de TypePromise para não conflitar com a implementação nativa de Promises, por enquanto ela é uma simples máquina de estados, considerando todos os estados que temos que ter:

import { PromiseStates, ResolveFunction, RejectFunction, ExecutorFunction, Nullable, Thennable } from './PromiseTypes'

export class TypePromise {
  private state: PromiseStates = PromiseStates.PENDING
  private value: any = null
  private thenHandlers: HandlerFunction[] = []
}
Enter fullscreen mode Exit fullscreen mode

Veja que temos um tipo novo, as HandlerFunctions são objetos que vamos passar quando tivermos diversos then ou catch em nossa promise. Nestas situações temos que executar os handlers um por um. Cada um deles é um objeto com as duas funções do executor, vamos adicionar no nosso arquivo PromiseTypes.ts e importar no arquivo principal, nosso arquivo PromiseTypes.ts fica assim:

import { TypePromise } from './TypePromise'

export enum PromiseStates {
  PENDING,
  FULFILLED,
  REJECTED
}

export type ResolveFunction = (value: any) => any
export type RejectFunction = (reason: any) => any
export type ExecutorFunction = (resolve: ResolveFunction, reject: RejectFunction) => void
export type Thennable = { then: (value: any) => TypePromise }

export type Nullable<T> = T | null

export type HandlerFunction = {
  onFulfilled?: ResolveFunction;
  onRejected?: Nullable<RejectFunction>
}
Enter fullscreen mode Exit fullscreen mode

Agora vamos transicionar nossa promise para os dois valores conhecidos, FULFILLED e REJECTED:

import { PromiseStates, ResolveFunction, RejectFunction, ExecutorFunction, Nullable, Thennable, HandlerFunction } from './PromiseTypes'

export class TypePromise {
  private state: PromiseStates = PromiseStates.PENDING
  private value: any = null
  private thenHandlers: HandlerFunction[] = []

  private fulfill (value: any) {
    this.state = PromiseStates.FULFILLED
    this.value = value
  }

  private reject (reason: any) {
    this.state = PromiseStates.REJECTED
    this.value = reason
  }
}
Enter fullscreen mode Exit fullscreen mode

Veja que as transições nada mais são do que alteradores de estado. São eles que vão finalizar uma promise e setar seu valor final.

Vamos agora criar uma outra transição chamada de resolve, esta transição será responsável por executar a promise em si e definir se ela foi resolvida ou rejeitada, bem como tratar se nossa promise receber outra promise.

import { PromiseStates, ResolveFunction, RejectFunction, ExecutorFunction, Nullable, Thennable, HandlerFunction } from './PromiseTypes'

export class TypePromise {
  private state: PromiseStates = PromiseStates.PENDING
  private value: any = null
  private thenHandlers: HandlerFunction[] = []

  private fulfill (value: any) {
    this.state = PromiseStates.FULFILLED
    this.value = value
  }

  private reject (reason: any) {
    this.state = PromiseStates.REJECTED
    this.value = reason
  }

  private resolve (result: any) {
    try {
        const then = this.getThen(result)
        if (then) return this.doResolve(then.bind(result), this.resolve, this.reject)
        this.fulfill(result)
    } catch (error) {
        this.reject(error)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Nossa função resolve é basicamente a responsável por saber se a função que recebemos é um objeto thennable, se sim, então irá chamar uma nova função chamada doResolve que leva um executor e faz um proxy da execução para os métodos internos da nossa própria promise, que serão chamados uma vez que a promise for resolvida, em poucas palavras, esta função é responsável por esperar que uma possível promise interna seja resolvida, uma vez que uma promise não pode ser resolvida com outra promise. Vamos implementar primeiramente o getThen, que é o responsável por extrair ou ignorar uma função thennable:

import { PromiseStates, ResolveFunction, RejectFunction, ExecutorFunction, Nullable, Thennable, HandlerFunction } from './PromiseTypes'

export class TypePromise {
  private state: PromiseStates = PromiseStates.PENDING
  private value: any = null
  private thenHandlers: HandlerFunction[] = []

  private fulfill (value: any) {
    this.state = PromiseStates.FULFILLED
    this.value = value
  }

  private reject (reason: any) {
    this.state = PromiseStates.REJECTED
    this.value = reason
  }

  private resolve (result: any) {
    try {
        const then = this.getThen(result)
        if (then) return this.doResolve(then.bind(result), this.resolve, this.reject)
        this.fulfill(result)
    } catch (error) {
        this.reject(error)
    }
  }

  private getThen (value: Thennable) {
    const valueType = typeof value
    if (value && (valueType === 'object' || valueType === 'function')) {
      const then = value.then
      if (typeof then === 'function') return then
    }
    return null
  }
}
Enter fullscreen mode Exit fullscreen mode

É uma função bem simples, ela recebe um objeto thennable e verifica se o tipo do valor passado é um objeto ou uma função, caso seja – e possua uma propriedade then – então verificamos se esta propriedade é uma função para podermos retornar somente este handler.

Vamos para o método doResolve, ele é o método principal da promise, porque é ele que vai iniciar toda a cadeia. Sendo responsável por garantir a única execução e também por criar wrappers ao redor das funções passadas pelo usuário para que elas possam ser controladas internamente.

import { 
  PromiseStates, 
  ResolveFunction, 
  RejectFunction, 
  ExecutorFunction, 
  Nullable, 
  Thennable, 
  HandlerFunction 
} from './PromiseTypes'

enum ReturnType {
  SUCCESS = 'success',
  ERROR = 'error'
}

export class TypePromise {
  private state: PromiseStates = PromiseStates.PENDING
  private value: any = null
  private thenHandlers: HandlerFunction[] = []

  private fulfill (value: any) {
    this.state = PromiseStates.FULFILLED
    this.value = value
  }

  private reject (reason: any) {
    this.state = PromiseStates.REJECTED
    this.value = reason
  }

  private resolve (result: any) {
    try {
        const then = this.getThen(result)
        if (then) return this.doResolve(then.bind(result), this.resolve, this.reject)
        this.fulfill(result)
    } catch (error) {
        this.reject(error)
    }
  }

  private getThen (value: Thennable) {
    const valueType = typeof value
    if (value && (valueType === 'object' || valueType === 'function')) {
      const then = value.then
      if (typeof then === 'function') return then
    }
    return null
  }

  private getHandlerType (type: ReturnType, done: boolean, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
    return (value: any) => {
      if (done) return
      done = true
      return { error: onRejected, success: onFulfilled }[type](value)
    }
  }

  private doResolve (resolverFn: ExecutorFunction, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
    let done = false
    try {
            resolverFn(this.getHandlerType(ReturnType.SUCCESS, done, onFulfilled, onRejected), this.getHandlerType(ReturnType.ERROR, done, onFulfilled, onRejected))
    } catch (error) {
      if (done) return
      done = true
      onRejected(error)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Agora fizemos algumas coisas a mais. Primeiramente nossa função doResolve basicamente checa se o nosso executor é confiável, ela faz isso criando wrappers ao redor das funções internas que temos para que elas somente sejam executadas uma única vez, isto é feito na nossa função getHandlerType que, basicamente, retorna uma nova função com assinatura (value: any) com uma checagem se ela já foi ou não executada alguma vez. Se sim, apenas retornamos, se não, vamos pegar a respectiva função de acordo com o tipo que queremos – ela pode ser uma função de resolução ou de rejeição, para fazer isso criamos no topo do arquivo o enum interno ReturnType – e vamos executá-la retornando seu valor.

Por fim, vamos fazer uma pequena alteração em nossa resolução de promises, de forma que possamos criar uma cadeia de promises a serem resolvidas. Para isso, vamos criar um método chamado executeHandler, que será responsável por receber e executar um objeto HandlerFunction, e a função auxiliar executeAllHandlers que irá iterar pelo nosso array de handlers e executar todos até que não haja mais nenhum:

import { 
  PromiseStates, 
  ResolveFunction, 
  RejectFunction, 
  ExecutorFunction, 
  Nullable, 
  Thennable, 
  HandlerFunction 
} from './PromiseTypes'

enum ReturnType {
  SUCCESS = 'success',
  ERROR = 'error'
}

export class TypePromise {
  private state: PromiseStates = PromiseStates.PENDING
  private value: any = null
  private thenHandlers: HandlerFunction[] = []

  private fulfill (value: any) {
    this.state = PromiseStates.FULFILLED
    this.value = value
        this.executeAllHandlers()
  }

  private reject (reason: any) {
    this.state = PromiseStates.REJECTED
    this.value = reason
    this.executeAllHandlers()
  }

  private executeAllHandlers () {
    this.thenHandlers.forEach(this.executeHandler)
    this.thenHandlers = []  
  }

  private executeHandler (handler: HandlerFunction) {
    if (this.state === PromiseStates.PENDING) return this.thenHandlers.push(handler)
    if (this.state === PromiseStates.FULFILLED && typeof handler.onFulfilled === 'function') return handler.onFulfilled(this.value)
    if (this.state === PromiseStates.REJECTED && typeof handler.onRejected === 'function') return handler.onRejected(this.value)
  }

  private resolve (result: any) {
    try {
        const then = this.getThen(result)
        if (then) return this.doResolve(then.bind(result), this.resolve, this.reject)
        this.fulfill(result)
    } catch (error) {
        this.reject(error)
    }
  }

  private getThen (value: Thennable) {
    const valueType = typeof value
    if (value && (valueType === 'object' || valueType === 'function')) {
      const then = value.then
      if (typeof then === 'function') return then
    }
    return null
  }

  private getHandlerType (type: ReturnType, done: boolean, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
    return (value: any) => {
      if (done) return
      done = true
      return { error: onRejected, success: onFulfilled }[type](value)
    }
  }

  private doResolve (resolverFn: ExecutorFunction, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
    let done = false
    try {
            resolverFn(this.getHandlerType(ReturnType.SUCCESS, done, onFulfilled, onRejected), this.getHandlerType(ReturnType.ERROR, done, onFulfilled, onRejected))
    } catch (error) {
      if (done) return
      done = true
      onRejected(error)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Com isso terminamos nossa máquina de estados.

Expondo métodos

Já que acabamos de construir nossa máquina de estados e suas transições, vamos agora criar os métodos que vão poder ser executados pelo usuário, como o then, catch e etc. Primeiramente vamos adicionar uma forma de resolver uma promise, criando seu construtor:

import { 
  PromiseStates, 
  ResolveFunction, 
  RejectFunction, 
  ExecutorFunction, 
  Nullable, 
  Thennable, 
  HandlerFunction 
} from './PromiseTypes'

enum ReturnType {
  SUCCESS = 'success',
  ERROR = 'error'
}

export class TypePromise {
  private state: PromiseStates = PromiseStates.PENDING
  private value: any = null
  private thenHandlers: HandlerFunction[] = []

  constructor (executor: ExecutorFunction) {
    this.resolve = this.resolve.bind(this)
    this.reject = this.reject.bind(this)
    this.executeHandler = this.executeHandler.bind(this)
    this.doResolve(executor, this.resolve, this.reject)
  }

  private fulfill (value: any) {
    this.state = PromiseStates.FULFILLED
    this.value = value
        this.executeAllHandlers()
  }

  private reject (reason: any) {
    this.state = PromiseStates.REJECTED
    this.value = reason
    this.executeAllHandlers()
  }

  private executeAllHandlers () {
    this.thenHandlers.forEach(this.executeHandler)
    this.thenHandlers = []  
  }

  private executeHandler (handler: HandlerFunction) {
    if (this.state === PromiseStates.PENDING) return this.thenHandlers.push(handler)
    if (this.state === PromiseStates.FULFILLED && typeof handler.onFulfilled === 'function') return handler.onFulfilled(this.value)
    if (this.state === PromiseStates.REJECTED && typeof handler.onRejected === 'function') return handler.onRejected(this.value)
  }

  private resolve (result: any) {
    try {
        const then = this.getThen(result)
        if (then) return this.doResolve(then.bind(result), this.resolve, this.reject)
        this.fulfill(result)
    } catch (error) {
        this.reject(error)
    }
  }

  private getThen (value: Thennable) {
    const valueType = typeof value
    if (value && (valueType === 'object' || valueType === 'function')) {
      const then = value.then
      if (typeof then === 'function') return then
    }
    return null
  }

  private getHandlerType (type: ReturnType, done: boolean, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
    return (value: any) => {
      if (done) return
      done = true
      return { error: onRejected, success: onFulfilled }[type](value)
    }
  }

  private doResolve (resolverFn: ExecutorFunction, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
    let done = false
    try {
            resolverFn(this.getHandlerType(ReturnType.SUCCESS, done, onFulfilled, onRejected), this.getHandlerType(ReturnType.ERROR, done, onFulfilled, onRejected))
    } catch (error) {
      if (done) return
      done = true
      onRejected(error)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Veja que nosso construtor simplesmente seta os valores iniciais e chama a função doResolve, dando inicio à resolução da promise.

Observando os valores

Para observarmos uma promise utilizamos then, para casos de sucesso, ou catch para casos de erro. Vamos criar nosso primeiro método público then:

import { 
  PromiseStates, 
  ResolveFunction, 
  RejectFunction, 
  ExecutorFunction, 
  Nullable, 
  Thennable, 
  HandlerFunction 
} from './PromiseTypes'

enum ReturnType {
  SUCCESS = 'success',
  ERROR = 'error'
}

export class TypePromise {
  private state: PromiseStates = PromiseStates.PENDING
  private value: any = null
  private thenHandlers: HandlerFunction[] = []

  constructor (executor: ExecutorFunction) {
    this.resolve = this.resolve.bind(this)
    this.reject = this.reject.bind(this)
    this.executeHandler = this.executeHandler.bind(this)
    this.doResolve(executor, this.resolve, this.reject)
  }

  private fulfill (value: any) {
    this.state = PromiseStates.FULFILLED
    this.value = value
        this.executeAllHandlers()
  }

  private reject (reason: any) {
    this.state = PromiseStates.REJECTED
    this.value = reason
    this.executeAllHandlers()
  }

  private executeAllHandlers () {
    this.thenHandlers.forEach(this.executeHandler)
    this.thenHandlers = []  
  }

  private executeHandler (handler: HandlerFunction) {
    if (this.state === PromiseStates.PENDING) return this.thenHandlers.push(handler)
    if (this.state === PromiseStates.FULFILLED && typeof handler.onFulfilled === 'function') return handler.onFulfilled(this.value)
    if (this.state === PromiseStates.REJECTED && typeof handler.onRejected === 'function') return handler.onRejected(this.value)
  }

  private resolve (result: any) {
    try {
        const then = this.getThen(result)
        if (then) return this.doResolve(then.bind(result), this.resolve, this.reject)
        this.fulfill(result)
    } catch (error) {
        this.reject(error)
    }
  }

  private getThen (value: Thennable) {
    const valueType = typeof value
    if (value && (valueType === 'object' || valueType === 'function')) {
      const then = value.then
      if (typeof then === 'function') return then
    }
    return null
  }

  private getHandlerType (type: ReturnType, done: boolean, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
    return (value: any) => {
      if (done) return
      done = true
      return { error: onRejected, success: onFulfilled }[type](value)
    }
  }

  private doResolve (resolverFn: ExecutorFunction, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
    let done = false
    try {
            resolverFn(this.getHandlerType(ReturnType.SUCCESS, done, onFulfilled, onRejected), this.getHandlerType(ReturnType.ERROR, done, onFulfilled, onRejected))
    } catch (error) {
      if (done) return
      done = true
      onRejected(error)
    }
  }

  then (onFulfilled?: ResolveFunction, onRejected?: Nulable<RejectFunction>): TypePromise {
    return new TypePromise((resolve: ResolveFunction, reject: RejectFunction) => {
      const handleResult = (type: ReturnType) => {
        return (result: any) => {
          try {
            const executorFunction = type === ReturnType.ERROR ? reject : resolve
            const checkFunction = type === ReturnType.ERROR ? onRejected : onFulfilled
            return (typeof checkFunction === 'function') ? executorFunction(checkFunction(result)) : executorFunction(result)
          } catch (error) {
            reject(error)
          }
        }
      }

      return this.done(handleResult(ReturnType.SUCCESS), handleResult(ReturnType.ERROR))
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Nosso método then implementa o que falamos anteriormente, ele recebe dois parâmetros opcionais e retorna uma instancia de uma nova promise. Novamente usamos a técnica do hashmap para podermos selecionar as funções que serão executadas. Caso as funções passadas pelo usuário sejam, de fato, funções, vamos executar estas funções primeiro e depois passar o resultado para o executor final que vai resolver a promise, se não, vamos apenas executar a função de resolução que pode ser reject ou resolve. Veja que temos um novo método, o done.

A função done(onFulfilled, onRejected) tem uma semântica mais simples de entender do que a do then, embora uma use a outra para que seja possível finalizar a promise. A função done segue as seguintes regras:

  • Somente um dos dois parâmetros é chamado
  • Só pode ser chamada uma vez

Esta função também só pode ser executada no final do tick do event loop então temos que ter certeza que ela sempre será agendada para esta execução. Para isso vamos usar a API process.nextTick que irá agendar a execução da função na fila de microtasks do Node (veja este guia para entender melhor) e será executado sempre no final do Event Loop:

import { 
  PromiseStates, 
  ResolveFunction, 
  RejectFunction, 
  ExecutorFunction, 
  Nullable, 
  Thennable, 
  HandlerFunction 
} from './PromiseTypes'

enum ReturnType {
  SUCCESS = 'success',
  ERROR = 'error'
}

export class TypePromise {
  private state: PromiseStates = PromiseStates.PENDING
  private value: any = null
  private thenHandlers: HandlerFunction[] = []

  constructor (executor: ExecutorFunction) {
    this.resolve = this.resolve.bind(this)
    this.reject = this.reject.bind(this)
    this.executeHandler = this.executeHandler.bind(this)
    this.doResolve(executor, this.resolve, this.reject)
  }

  private fulfill (value: any) {
    this.state = PromiseStates.FULFILLED
    this.value = value
        this.executeAllHandlers()
  }

  private reject (reason: any) {
    this.state = PromiseStates.REJECTED
    this.value = reason
    this.executeAllHandlers()
  }

  private executeAllHandlers () {
    this.thenHandlers.forEach(this.executeHandler)
    this.thenHandlers = []  
  }

  private executeHandler (handler: HandlerFunction) {
    if (this.state === PromiseStates.PENDING) return this.thenHandlers.push(handler)
    if (this.state === PromiseStates.FULFILLED && typeof handler.onFulfilled === 'function') return handler.onFulfilled(this.value)
    if (this.state === PromiseStates.REJECTED && typeof handler.onRejected === 'function') return handler.onRejected(this.value)
  }

  private resolve (result: any) {
    try {
        const then = this.getThen(result)
        if (then) return this.doResolve(then.bind(result), this.resolve, this.reject)
        this.fulfill(result)
    } catch (error) {
        this.reject(error)
    }
  }

  private getThen (value: Thennable) {
    const valueType = typeof value
    if (value && (valueType === 'object' || valueType === 'function')) {
      const then = value.then
      if (typeof then === 'function') return then
    }
    return null
  }

  private getHandlerType (type: ReturnType, done: boolean, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
    return (value: any) => {
      if (done) return
      done = true
      return { error: onRejected, success: onFulfilled }[type](value)
    }
  }

  private doResolve (resolverFn: ExecutorFunction, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
    let done = false
    try {
            resolverFn(this.getHandlerType(ReturnType.SUCCESS, done, onFulfilled, onRejected), this.getHandlerType(ReturnType.ERROR, done, onFulfilled, onRejected))
    } catch (error) {
      if (done) return
      done = true
      onRejected(error)
    }
  }

  then (onFulfilled?: ResolveFunction, onRejected?: Nulable<RejectFunction>): TypePromise {
    return new TypePromise((resolve: ResolveFunction, reject: RejectFunction) => {
      const handleResult = (type: ReturnType) => {
        return (result: any) => {
          try {
            const executorFunction = type === ReturnType.ERROR ? reject : resolve
            const checkFunction = type === ReturnType.ERROR ? onRejected : onFulfilled
            return (typeof checkFunction === 'function') ? executorFunction(checkFunction(result)) : executorFunction(result)
          } catch (error) {
            reject(error)
          }
        }
      }

      return this.done(handleResult(ReturnType.SUCCESS), handleResult(ReturnType.ERROR))
    })
  }

  private done (onFulfilled?: ResolveFunction, onRejected?: Nullable<RejectFunction>) {
    process.nextTick(() => {
      this.executeHandler({
        onFulfilled,
        onRejected
      })
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Este método nada mais faz do que executar o nosso handler then no final do tick.

Catch

Para finalizar, vamos implementar o método catch para que o usuário possa capturar os erros da promise. Ele é bastante simples e também tira proveito da função done, diferentemente do then, o catch sempre terá um argumento cujo tipo é uma função de rejeição:

import { 
  PromiseStates, 
  ResolveFunction, 
  RejectFunction, 
  ExecutorFunction, 
  Nullable, 
  Thennable, 
  HandlerFunction 
} from './PromiseTypes'

enum ReturnType {
  SUCCESS = 'success',
  ERROR = 'error'
}

export class TypePromise {
  private state: PromiseStates = PromiseStates.PENDING
  private value: any = null
  private thenHandlers: HandlerFunction[] = []

  constructor (executor: ExecutorFunction) {
    this.resolve = this.resolve.bind(this)
    this.reject = this.reject.bind(this)
    this.executeHandler = this.executeHandler.bind(this)
    this.doResolve(executor, this.resolve, this.reject)
  }

  private fulfill (value: any) {
    this.state = PromiseStates.FULFILLED
    this.value = value
        this.executeAllHandlers()
  }

  private reject (reason: any) {
    this.state = PromiseStates.REJECTED
    this.value = reason
    this.executeAllHandlers()
  }

  private executeAllHandlers () {
    this.thenHandlers.forEach(this.executeHandler)
    this.thenHandlers = []  
  }

  private executeHandler (handler: HandlerFunction) {
    if (this.state === PromiseStates.PENDING) return this.thenHandlers.push(handler)
    if (this.state === PromiseStates.FULFILLED && typeof handler.onFulfilled === 'function') return handler.onFulfilled(this.value)
    if (this.state === PromiseStates.REJECTED && typeof handler.onRejected === 'function') return handler.onRejected(this.value)
  }

  private resolve (result: any) {
    try {
        const then = this.getThen(result)
        if (then) return this.doResolve(then.bind(result), this.resolve, this.reject)
        this.fulfill(result)
    } catch (error) {
        this.reject(error)
    }
  }

  private getThen (value: Thennable) {
    const valueType = typeof value
    if (value && (valueType === 'object' || valueType === 'function')) {
      const then = value.then
      if (typeof then === 'function') return then
    }
    return null
  }

  private getHandlerType (type: ReturnType, done: boolean, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
    return (value: any) => {
      if (done) return
      done = true
      return { error: onRejected, success: onFulfilled }[type](value)
    }
  }

  private doResolve (resolverFn: ExecutorFunction, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
    let done = false
    try {
            resolverFn(this.getHandlerType(ReturnType.SUCCESS, done, onFulfilled, onRejected), this.getHandlerType(ReturnType.ERROR, done, onFulfilled, onRejected))
    } catch (error) {
      if (done) return
      done = true
      onRejected(error)
    }
  }

  then (onFulfilled?: ResolveFunction, onRejected?: Nulable<RejectFunction>): TypePromise {
    return new TypePromise((resolve: ResolveFunction, reject: RejectFunction) => {
      const handleResult = (type: ReturnType) => {
        return (result: any) => {
          try {
            const executorFunction = type === ReturnType.ERROR ? reject : resolve
            const checkFunction = type === ReturnType.ERROR ? onRejected : onFulfilled
            return (typeof checkFunction === 'function') ? executorFunction(checkFunction(result)) : executorFunction(result)
          } catch (error) {
            reject(error)
          }
        }
      }

      return this.done(handleResult(ReturnType.SUCCESS), handleResult(ReturnType.ERROR))
    })
  }

  private done (onFulfilled?: ResolveFunction, onRejected?: Nullable<RejectFunction>) {
    process.nextTick(() => {
      this.executeHandler({
        onFulfilled,
        onRejected
      })
    })
  }

  catch (onRejected: RejectFunction) {
    return new TypePromise((resolve: ResolveFunction, reject: RejectFunction) => {
      return this.done(resolve, (error: any) => {
        if(typeof onRejected === 'function') {
          try {
            return resolve(onRejected(error))
          } catch (error) {
            reject(error)
          }
        }
        return reject(error)
      })
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Também retornamos uma nova promise e verificamos se a função passada não é uma outra promise que precisamos resolver antes.

Bonus: Finally

Há algum tempo a especificação dopromise.prototype.finally entrou em vigor e já está funcionando no ES6. A função finally tem um objetivo simples: Assim como o finally que temos em um bloco try/catch, o finally de uma promise é sempre executado no final de sua resolução, independente se a promise foi resolvida ou rejeitada, porém, diferentemente dos observadores como then e catch, o finally não retorna outra promise então não é possível encadear chamadas após o finally ser executado.

A implementação é relativamente simples, vamos criar uma propriedade em nossa promise chamada finalFunction, que começará como uma função vazia. O objetivo é termos um método finally que irá trocar o valor desta propriedade para a função que o usuário passar e então vai executá-la no final dos métodos fulfill ou reject:

import { 
  PromiseStates, 
  ResolveFunction, 
  RejectFunction, 
  ExecutorFunction, 
  Nullable, 
  Thennable, 
  HandlerFunction 
} from './PromiseTypes'

enum ReturnType {
  SUCCESS = 'success',
  ERROR = 'error'
}

export class TypePromise {
  private state: PromiseStates = PromiseStates.PENDING
  private finalFunction: Function = () => { }
  private value: any = null
  private thenHandlers: HandlerFunction[] = []

  constructor (executor: ExecutorFunction) {
    this.resolve = this.resolve.bind(this)
    this.reject = this.reject.bind(this)
    this.executeHandler = this.executeHandler.bind(this)
    this.doResolve(executor, this.resolve, this.reject)
  }

  private fulfill (value: any) {
    this.state = PromiseStates.FULFILLED
    this.value = value
        this.executeAllHandlers()
    this.finalFunction() // Executamos o finally
  }

  private reject (reason: any) {
    this.state = PromiseStates.REJECTED
    this.value = reason
    this.executeAllHandlers()
    this.finalFunction() // Executamos o finally
  }

  private executeAllHandlers () {
    this.thenHandlers.forEach(this.executeHandler)
    this.thenHandlers = []  
  }

  private executeHandler (handler: HandlerFunction) {
    if (this.state === PromiseStates.PENDING) return this.thenHandlers.push(handler)
    if (this.state === PromiseStates.FULFILLED && typeof handler.onFulfilled === 'function') return handler.onFulfilled(this.value)
    if (this.state === PromiseStates.REJECTED && typeof handler.onRejected === 'function') return handler.onRejected(this.value)
  }

  private resolve (result: any) {
    try {
        const then = this.getThen(result)
        if (then) return this.doResolve(then.bind(result), this.resolve, this.reject)
        this.fulfill(result)
    } catch (error) {
        this.reject(error)
    }
  }

  private getThen (value: Thennable) {
    const valueType = typeof value
    if (value && (valueType === 'object' || valueType === 'function')) {
      const then = value.then
      if (typeof then === 'function') return then
    }
    return null
  }

  private getHandlerType (type: ReturnType, done: boolean, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
    return (value: any) => {
      if (done) return
      done = true
      return { error: onRejected, success: onFulfilled }[type](value)
    }
  }

  private doResolve (resolverFn: ExecutorFunction, onFulfilled: ResolveFunction, onRejected: RejectFunction) {
    let done = false
    try {
            resolverFn(this.getHandlerType(ReturnType.SUCCESS, done, onFulfilled, onRejected), this.getHandlerType(ReturnType.ERROR, done, onFulfilled, onRejected))
    } catch (error) {
      if (done) return
      done = true
      onRejected(error)
    }
  }

  then (onFulfilled?: ResolveFunction, onRejected?: Nulable<RejectFunction>): TypePromise {
    return new TypePromise((resolve: ResolveFunction, reject: RejectFunction) => {
      const handleResult = (type: ReturnType) => {
        return (result: any) => {
          try {
            const executorFunction = type === ReturnType.ERROR ? reject : resolve
            const checkFunction = type === ReturnType.ERROR ? onRejected : onFulfilled
            return (typeof checkFunction === 'function') ? executorFunction(checkFunction(result)) : executorFunction(result)
          } catch (error) {
            reject(error)
          }
        }
      }

      return this.done(handleResult(ReturnType.SUCCESS), handleResult(ReturnType.ERROR))
    })
  }

  private done (onFulfilled?: ResolveFunction, onRejected?: Nullable<RejectFunction>) {
    process.nextTick(() => {
      this.executeHandler({
        onFulfilled,
        onRejected
      })
    })
  }

  catch (onRejected: RejectFunction) {
    return new TypePromise((resolve: ResolveFunction, reject: RejectFunction) => {
      return this.done(resolve, (error: any) => {
        if(typeof onRejected === 'function') {
          try {
            return resolve(onRejected(error))
          } catch (error) {
            reject(error)
          }
        }
        return reject(error)
      })
    })
  }

  finally (finalFunction: Function) {
    if (typeof finalFunction === 'function') this.finalFunction = finalFunction
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusão

Criamos nossa própria implementação de uma função em Typescript. Com isso temos a vantagem de ter tipagens em tempo de desenvolvimento, porém ainda precisamos checar os tipos durante a execução da função porque, afinal, todo o código é transpilado para JavaScript quando fizermos o build do mesmo. Podemos usar nossa promise desta forma:

import { TypePromise } from './TypePromise'

function foo (param: any) {
  return new TypePromise((resolve, reject) => {
    if (Math.random() > 0.5) return setTimeout(resolve, 1000, param)
    return setTimeout(reject, 1000, 'error')
  })
}

(() => {
  foo(5)
    .then((value) => console.log(value))
    .catch((error) => console.error(error))
    .finally(() => console.log('aways return'))
})()
Enter fullscreen mode Exit fullscreen mode

Note também que estamos utilizando any como tipo em vários casos, isto não é uma boa prática ao se utilizar Typescript porque deixamos a tipagem de lado. Então, como uma lição de casa, um desafio bacana seria implementar os tipos genéricos para que class TypePromise passe a ser class TypePromise<T>. Vamos abordar essa correção na sequencia deste artigo!

Se você estiver interessado em aprender mais, dê uma olhada nestas referências:

Não deixe de acompanhar mais do meu conteúdo no meu blog e se inscreva na newsletter para receber notícias semanais!

Top comments (0)