TypeScript는 프론트엔드 개발을 더 엄격하게 하고 리팩토링을 쉽게 만들어준다. 하지만 예상보다 훨씬 유연한 언어이기도 하다. 이 글에서는 많은 사람들이 간과할 수 있는 TypeScript의 5가지 유용한 트릭을 소개하려고 한다.
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.
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를 지정할 수 없다는 점도 기억해야 한다.
제네릭을 사용하면 코드의 유연성이 높아지지만, 항상 타입을 명시해야 하는 것은 아니다.
기본값을 지정하면 더욱 깔끔한 코드 작성이 가능하다.
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 타입이 적용된다.
기본값을 설정하면 제네릭 타입을 생략할 수 있어 가독성이 향상된다.
- 수정자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
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();