avatar
Published on

Array vs ArrayLike, Promise vs PromiseLike

Author
  • avatar
    Name
    yceffort

타입스크립트에는 ArrayLike라는게 존재한다. Array는 일반적인 배열을 의미하는데, ArrayLike는 무엇일까? 이를 알아보기 위해 lib.es5.d.ts에 가서 각각의 스펙을 살펴보자.

Array

ArrayLike<T>

interface ArrayLike<T> {
  readonly length: number
  readonly [n: number]: T
}

Array<T>

interface Array<T> {
  /**
   * Returns the value of the first element in the array where predicate is true, and undefined
   * otherwise.
   * @param predicate find calls predicate once for each element of the array, in ascending
   * order, until it finds one where predicate returns true. If such an element is found, find
   * immediately returns that element value. Otherwise, find returns undefined.
   * @param thisArg If provided, it will be used as the this value for each invocation of
   * predicate. If it is not provided, undefined is used instead.
   */
  find<S extends T>(
    predicate: (this: void, value: T, index: number, obj: T[]) => value is S,
    thisArg?: any,
  ): S | undefined
  find(
    predicate: (value: T, index: number, obj: T[]) => unknown,
    thisArg?: any,
  ): T | undefined

  /**
   * Returns the index of the first element in the array where predicate is true, and -1
   * otherwise.
   * @param predicate find calls predicate once for each element of the array, in ascending
   * order, until it finds one where predicate returns true. If such an element is found,
   * findIndex immediately returns that element index. Otherwise, findIndex returns -1.
   * @param thisArg If provided, it will be used as the this value for each invocation of
   * predicate. If it is not provided, undefined is used instead.
   */
  findIndex(
    predicate: (value: T, index: number, obj: T[]) => unknown,
    thisArg?: any,
  ): number

  /**
   * Changes all array elements from `start` to `end` index to a static `value` and returns the modified array
   * @param value value to fill array section with
   * @param start index to start filling the array at. If start is negative, it is treated as
   * length+start where length is the length of the array.
   * @param end index to stop filling the array at. If end is negative, it is treated as
   * length+end.
   */
  fill(value: T, start?: number, end?: number): this

  /**
   * Returns the this object after copying a section of the array identified by start and end
   * to the same array starting at position target
   * @param target If target is negative, it is treated as length+target where length is the
   * length of the array.
   * @param start If start is negative, it is treated as length+start. If end is negative, it
   * is treated as length+end.
   * @param end If not specified, length of the this object is used as its default value.
   */
  copyWithin(target: number, start: number, end?: number): this
}

Array는 딱봐도 우리가 일반적으로 아는 배열에 들어가는 메소드들이 정의되어 있지만, ArrayLike는 그렇지 않다. length와 index로만 접근할 수 있도록 구현되어 있다. 이는 바로 우리가 잘 알고 있는 유사 배열 객체다. 배열 처럼 순회할 수 있지만, 그 뿐인 유사 배열 객체. 대표적으로는

가 있다.

Promise

그렇다면 이번에는 Promise를 살펴보자.

Promise<T> (lib.2018.promise.d.ts)

interface Promise<T> {
  /**
   * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The
   * resolved value cannot be modified from the callback.
   * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected).
   * @returns A Promise for the completion of the callback.
   */
  finally(onfinally?: (() => void) | undefined | null): Promise<T>
}

Promise<T> (lib.es5.d.ts)

interface Promise<T> {
  /**
   * Attaches callbacks for the resolution and/or rejection of the Promise.
   * @param onfulfilled The callback to execute when the Promise is resolved.
   * @param onrejected The callback to execute when the Promise is rejected.
   * @returns A Promise for the completion of which ever callback is executed.
   */
  then<TResult1 = T, TResult2 = never>(
    onfulfilled?:
      | ((value: T) => TResult1 | PromiseLike<TResult1>)
      | undefined
      | null,
    onrejected?:
      | ((reason: any) => TResult2 | PromiseLike<TResult2>)
      | undefined
      | null,
  ): Promise<TResult1 | TResult2>

  /**
   * Attaches a callback for only the rejection of the Promise.
   * @param onrejected The callback to execute when the Promise is rejected.
   * @returns A Promise for the completion of the callback.
   */
  catch<TResult = never>(
    onrejected?:
      | ((reason: any) => TResult | PromiseLike<TResult>)
      | undefined
      | null,
  ): Promise<T | TResult>
}

PromiseLike<T>

interface PromiseLike<T> {
  /**
   * Attaches callbacks for the resolution and/or rejection of the Promise.
   * @param onfulfilled The callback to execute when the Promise is resolved.
   * @param onrejected The callback to execute when the Promise is rejected.
   * @returns A Promise for the completion of which ever callback is executed.
   */
  then<TResult1 = T, TResult2 = never>(
    onfulfilled?:
      | ((value: T) => TResult1 | PromiseLike<TResult1>)
      | undefined
      | null,
    onrejected?:
      | ((reason: any) => TResult2 | PromiseLike<TResult2>)
      | undefined
      | null,
  ): PromiseLike<TResult1 | TResult2>
}

Promise<T>에는 finally만 있고, PromiseLike<T>에는 then 밖에 없다. 🤔 이 둘의 차이를 먼저 알 필요가 있다.

then vs finally

  • finally: promise가 처리되면 충족되거나 (resolve) 거부되거나 (reject) 상관없이 실행하는 콜백함수다. Promise의 성공적으로 수행되었는지, 거절되었는지에 관계없이 Promise가 처리된 후에 무조건 한번은 실행되는 코드다.
  • then: 은 우리가 잘 아는 것처럼 Promise를 리턴하고 두개의 콜백함수를 받는다. 하나는 충족되었을 때 (resolve) 그리고 거부되었을 때 (reject)를 위한 콜백 함수다.
p.then(onFulfilled, onRejected)

p.then(
  function (value) {
    // 이행
  },
  function (reason) {
    // 거부
  },
)

그리고 또한가지는 finally는 Promise 체이닝에서 결과를 받을 수 없다는 것이다.

const result = new Promise((resolve, reject) => resolve(10))
  .then((x) => {
    console.log(x) // 10
    return x + 1
  })
  .finally((x) => {
    console.log(x) // undefined
    return x + 2
  })
// then에서 리턴했던 11을 resolve 한다.
result // Promise {<fulfilled>: 11}

또다른 차이는 에러핸들링과 Promise chaining이다. 만약 promise chaining에서 에러처리를 미루고 다른 어딘가에서 처리하고 싶다면, finally를 사용하면 된다.

new Promise((resolve, reject) => reject(0))
  .catch((x) => {
    console.log(x) // 0
    throw x
  })
  .then((x) => {
    console.log(x) // Will not run
  })
  .finally(() => {
    console.log('clean up') // 'clean up'
  })
// Uncaught (in promise) 0
// try catch 로 잡으면 잡힌다!

끝으로 finally는 es2018에서 나온 메소드 이기 때문에 lib.es2018.promise.d.ts에 존재한다. https://2ality.com/2017/07/promise-prototype-finally.html

아무튼 다시 돌아가서, catch가 없는 PromiseLike는 왜 존재하는 것일까? 🤔 Promise가 정식 스펙이 되기 전, Promise를 구현하기 위한 다양한 라이브러리가 존재했다.

이들은 표준이전에 태어나 catch 구문없이 promise를 처리하고 있었고, 타입스크립트는 이를 지원하기 위해서 PromiseLike를 만든 것이었다.

따라서 Promise 뿐만 아니라 좀더 광의의 Promise (표준 이전에 만들어진 라이브러리로 만들어진 Promise)를 처리하기 위해서 PromiseLike 타입을 추가하게 된 것이다.