[번역] 5 TypeScript Coding Tips You Might Ignore

임광훈·2025년 2월 4일

원문

당신이 놓쳤을지도 모르는 5가지 TypeScript 팁

TypeScript는 프론트엔드 개발을 더 엄격하게 하고 리팩토링을 쉽게 만들어준다. 하지만 예상보다 훨씬 유연한 언어이기도 하다. 이 글에서는 많은 사람들이 간과할 수 있는 TypeScript의 5가지 유용한 트릭을 소개하려고 한다.

1. 더 나은 코드 주석 달기

TypeScript에서는 /** ... */ 형식의 JSDoc 스타일 주석을 사용하여 타입 정보를 명확하게 설명할 수 있다. 이를 활용하면 코드 편집기에서 더 나은 자동 완성 및 힌트를 제공해 준다.

export interface RequestOptions {
  /**
   * Request method
   * @default 'GET'
   */
  method?: string;
  /**
   * Request body
   * @default null
   */
  body?: string | null;
  /**
   * Request headers
   */
  headers?: Record<string, string | string[]>;

  /** Request query */
  query?: Record<string, string>;
}

const options: RequestOptions = {
  method: 'GET',
  body: '',
};

위 코드에서 /** ... */ 주석을 작성한 곳에 마우스를 올려보면 해당 설명이 툴팁으로 표시된다. 이는 코드의 가독성을 높이고, 협업 시 다른 개발자들에게도 큰 도움이 된다.

하지만 불필요한 주석은 코드의 유지보수를 어렵게 만들 수도 있다. 주석을 달기 전에 코드를 개선할 방법이 있는지 고민해보자. 관련 글:
Stop Commenting Bad Code. Rewrite It.

2. this 파라미터 명시하기

다음 코드를 보자:

export class Test {
  name = 'test';

  printName() {
    console.log(this.name);
  }
}

const test = new Test();

// 예상과 다르게 'fake'가 출력됨
test.printName.call({ name: 'fake' });

이 코드에서 test.printName.call({ name: 'fake' })를 실행하면 this.name은 "test"가 아닌 "fake"로 출력된다.
TypeScript는 기본적으로 call, apply를 사용할 때 this가 어떤 객체를 참조하는지 검사하지 않기 때문이다.

이를 방지하려면 this 타입을 명시적으로 지정해줄 수 있다.

class Test {
  name = 'test';
  printName(this: Test) {
    console.log(this.name);
  }
}
const test = new Test();
// 오류 발생: '{ name: string; }' 타입은 'Test' 타입에 할당될 수 없음
test.printName.call({ name: 'fake' });

이제 TypeScript가 올바르지 않은 this 값을 감지하고 경고해준다. 하지만 화살표 함수(arrow function)에서는 this를 지정할 수 없다는 점도 기억해야 한다.

3. 제네릭(Generic) 파라미터 기본값 설정

제네릭을 사용하면 코드의 유연성이 높아지지만, 항상 타입을 명시해야 하는 것은 아니다.
기본값을 지정하면 더욱 깔끔한 코드 작성이 가능하다.

export class Test<T = string> {
  #list: T[] = [];

  add = (e: T) => {
    this.#list.push(e);
  };

  log = () => {
    console.log(this.#list);
  };
}

const test = new Test();  // 기본값으로 string이 사용됨
test.add('1');  // OK
test.add(1);    // 오류 발생
test.add({});   // 오류 발생

const numTest = new Test<number>(); // 명시적으로 number 지정
numTest.add(1);   // OK
numTest.add('1'); // 오류 발생
numTest.add({});  // 오류 발생

위 코드에서 Test<string>을 명시하지 않아도 기본적으로 string 타입이 적용된다.
기본값을 설정하면 제네릭 타입을 생략할 수 있어 가독성이 향상된다.

4. - 수정자

TypeScript의 내장 유틸리티 타입인 Partial<T>Required<T>는 객체의 프로퍼티를 선택적(optional) 또는 필수(required)로 변환하는 데 사용된다.

이들의 정의를 살펴보면 다음과 같다.

/**
 * Make all properties in T optional
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};

/**
 * Make all properties in T required
 */
type Required<T> = {
    [P in keyof T]-?: T[P];
};

여기서 -??(선택적) 수정자를 제거하는 역할을 한다.

이와 유사하게 -readonly를 사용하면 읽기 전용 프로퍼티를 변경 가능하게 만들 수도 있다.

/**
     * Creates a Promise that is resolved with an array of results when all of the provided Promises
     * resolve, or rejected when any Promise is rejected.
     * @param values An array of Promises.
     * @returns A new Promise.
     */
all<T extends readonly unknown[] | []>(values: T): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>

추가적으로 TypeScript의 유틸리티 타입에 대해 더 알아보고 싶다면 다음 글을 참고하자:
7 TypeScript Built-in Utility Types You Must Know

5. 어설션(Assertion) 함수 사용하기

Html 요소를 가져오는 방식은 document.getElementById 혹은 document.querySelector이고, 우리는 HTMLElement | null 타입을 반환 받는다.

/**
* Returns a reference to the first object with the specified value of the ID attribute.
* @param elementId String that specifies the ID value.
*/
getElementById(elementId: string): HTMLElement | null;

여기서 선택된 element에 이벤트를 바인딩 하거나, 정보를 가져오려고 할때 element는 null이 아니여야 한다.

그럴때 우리는 첫번째로 !를 통해 non-null어설션을 줄 수 있다.

const element = document.getElementById("test")!

하지만 non-null어설션을 사용하는것은 안전하지 않고, JavaScript로 컴파일 시 제거된다. 이 상황에서 element가 존재하지 않는다면 에러가 발생한다. non-null어설션은 element가 존재한다고 확신하기 전 까지는 비추천한다.

그 외에도 우리는 사용하기 전에 null check를 할 수 있다.

const element = document.getElementById("test")

if (!element) {
  throw new Error("This element was not found")
}

element.clientHeight

우리가 공통함수로 이를 실행하려고 한다면:

const isDef = (value: any) => value! == void 0 && value! == null

const element = document.getElementById("test")

if (!isDef(element)) {
  throw new Error("This element was not found")
}

// Object is possibly 'null'. ts(2531)
element.clientHeight

코드의 로직은 맞지만, TypeScript 컴파일러는 이를 불평한다. 이때 우리는 null값을 제거하기 위해 is키워드를 사용할 수 있다.

const isDef = <T>(value: T): value is NonNullable<T> => value! == void 0 && value! == null;

const element = document.getElementById("test")

if (!isDef(element)) {
  throw new Error("This element was not found")
}

element.clientHeight

추가로 이때 어설션 펑션을 사용할 수 있다:

function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
  if (value! == void 0 && value! == null) {
    throw new Error("This element was not found")
  }
}

const element = document.getElementById("test")

assertIsDefined(element)

element.clientHeight

이런식으로 함수선언을 사용하면 TypeScript가 assertIsDefined가 어설션 함수임을 알 수 있다. 이 함수는 value가 non-null임을 주장할 수 있고, 아니라면 에러가 발생한다. asserts키워드는 이렇게 우리를 도와준다.

non-null뿐만 아니라 우리는 또한 다른 유형도 assert 할 수 있다.

// Function expressions require explicit type annotations.
type AssertIsString = (val: any) => asserts val is string;
const assertIsString: AssertIsString = (val) => {
  if (typeof val !== 'string') {
    throw Error('Not a string');
  }
};

// In some cases you don't need to throw an error, you can just use `is`.
const isString = (val: any): val is string => {
  return typeof val === 'string';
};

const test: unknown = {};

// if (isString(test)) {
//   test.toUpperCase();
// }

// assertIsString(test);
test.toUpperCase();
profile
나, 가능

0개의 댓글