IT/언어

[TypeScript] (React의 사용없이)실전적인 기술 모음

개발자 두더지 2023. 9. 21. 22:56
728x90

일본의 한 블로그 글을 번역한 포스트입니다. 오역 및 의역, 직역이 존재할 수 있으며 틀린 내용은 지적해주시면 감사하겠습니다.

 

지금까지 알게 된 사소하고 자잘한 TypeScript관련 테크닉을 모은 포스트이다.

 

자신이 정의한 타입에 대해 타입 가드하기


유저 정의 타입 가드에 대해서

 TypeScript에서는 typeof, instanceof, in등의 연산자를 이용하여 변수에 대해 타입 가드할 수 있지만, 이러한 연산자로는 자신이 정의한 타입을 가드할 수 없다.

type Hoge = "hoge" | "fuga";

const attr = document.querySelector(".hoge")?.getAttribute("name") ?? "";
if (typeof attr === Hoge) {
  // NG
  console.log(attr);
}

 이러한 경우에 유저 정의 타입 가드를 이용하여 타입 가드한다. 유저 정의 타입 가드란 자신이 정의한 데이터 형에 맞게 조건식을 스스로 작성하고, 그 조건식으로 타입 가드하는 방법이다. 

 인수를 받아 그 인수가 자신이 정의한 데이터에 매치하는지 아닌지를 판단하여 그 결과를 boolean을 반환하는 함수를 만든다. 그 함수의 반환 값의 데이터 타입을 is 연산자를 이용하 {인수} is {데이터 타입}을 지정한다. 이것으로 데이터 타입 가드의 완성이다.

 이 함수에 인수를 전달하여 true를 반환할 경우, 인수 x는 자신이 정의한 타입으로 타입 가드된다.

type Hoge = "hoge" | "fuga";
const isHoge = (x: string): x is Hoge => ["hoge", "fuga"].some((val) => val === x);

let attr: string;
if (isHoge(attr)) {
  attr; // 이 블록 내에서는 `"hoge" | "fuga"로 타입가드된다.
}

 

Union 타입의 유저 정의 타입 가드 기술

 자신이 정의한 Union 타입으로 타입 가드할 때 사용할 수 있는 유용한 테크닉이 있다. Union 데이터타입이 다룰 값을 배열에 저장하고,  typeof Array[number]에서 Union타입을 정의한다. 그리고 그 배열에서 유저 정의 데이터 타입 가드를 작성하는 방법이다.

const hogeArray = ["hoge", "fuga"] as const; // const Assertion가 필요
type Hoge = (typeof hogeArray)[number]; // Hoge = "hoge" | "fuga"
const isHoge = (x: string): x is Hoge => hogeArray.some((val) => val === x);

let attr: string;
if (isHoge(attr)) {
  attr; // 이 블록내에서 `"hoge" | "fuga"`으로 타입 가드된다.
}

 const Assertion한 배열에 대해 type of Array[number]하면, 그 배열의 요소의 Union형을 꺼낼 수 있다.  배열을 한 번 정의해두면 Array.some()으로 간단히 유저 정의 데이터 가드를 만들 수 있다. 

 array.some((val) => val === x)가 true인 경우 x는 array의 요소 중 일치하는 것이 있다, 즉 (typeof array)[number] 데이터형이다라는 의미가 된다.

 이 구현 방법의 유용한 점은 데이터 타입 정보에 변화가 있을 때 배열의 요소를 수정하는 것만으로도 가드할 수 있기 때문에 보수성이 담보된다는 점이다.

 

 

DOM에서 획득한 요소의 데이터 타입을 instanceof 연산자로 특정하기


 이 부분도 타입 가드에 관련된 내용이다. JavaScript에서는 문제 없이 액세스한 DOM의 속성인데, TypeScript에서는 액세스할 수 없는 경우가 꽤 있다.

const anchorElm = document.querySelector(".anchorElm"); // 어떠한 a요소를 상성
anchorElm.href; // NG: Property 'href' does not exist on type 'Element'.

 위의 예는 document.querySelector()으로 DOM 상의 a 요소를 획득하여 href 속성에 액세스하려고 하고 있지만 "Element 타입에는 href 속성이 존재하지 않습니다"라는 에러가 발생한다. 

 이것은 document.querySelector()의 반환값이 Element 타입으로, href 속성은 Element의 인스턴스 속성으로 정의되어 있지 않은 것이 원인이다.

 여기서 instanceof 연사자를 사용하여 요소를 타입 가드하는 것으로 Element에서 상속 대상 클래스에만 정의된 인스턴스 속성에 액세스할 수 있다.

 아래의 예에서는 Element타입을 href 속성이 정의되어 있는 HTMLAnchorElement 타입으로 타입 가드하고 있다.

const anchorElm = document.querySelector(".anchorElm");
if (anchorElm instanceof HTMLAnchorElement) {
  anchorElm.href; // OK
}

 Property 'xxx' does not exist on type 'xxx'. 에러가 발생하면, 액세스하고 싶은 속성이 어떤 타입으로 정의되어 있는지를 확인하여 instanceof 연산자를 사용하여 요소를 타입 가드하자.

 

 

Array.filter()으로 타입 가드하는 방법


Array.filter()의 반환값 데이터 형을 is 연산자로 지정

 Array.filter() 메소드로 배열의 필터링이 가능하지만, 타입 정보까지 필터링할 수 없다.

const array = [1, "2", "3", 4, 5];
const filtered = array.filter((val) => typeof val === "string");
filtered; // (string | number)[] → string[]이 되었음한다.

 여기서도 유저 정의 타입 가드와 동일하게 .filter() 메소드의 반환값 타입을 is 연산자로 지정하는 것으로 필러링 후의 배열 요소 타입 가드가 가능하다.

const array = [1, "2", "3", 4, 5];
const filtered = array.filter((val): val is string => typeof val === "string");
filtered; // string[]

 

필터링 조건에 따라 타입 정보를 반환하면 된다.

 위의 예에서는 "타입이 string 요소만을 추출"하는 조건식이므로 is string이라는 타입 가드가 적당하다. 한편, 조건식을 "타입이 number가 아닌 요소만을 추출"한다고 하고 싶을 경우, 타입 정의도 "요소의 데이터 타입에서 number 타입을 배제한 데이터형이라는 필터링 조건"에 따라 타입을 지정하면 된다.

const array = [1, "2", "3", 4, 5];
const filtered = array.filter((val): val is Exclude<typeof val, number> => typeof val !== "number");
filtered; // string[]

 어떤 타입에서 특정 타입을 배제하고 싶은 경우 Exclude<T, U>나 Omit<T, U>를 이용하자.

 

 

템플릿 리터럴을 const Assertion하여 보다 압축된 타입 정보로 나타내기


 템플릿 리터럴로 선언한 변수는 string형이 되지만, 그 변수를 const Assertion하면 보다 압축된 문자열 리터럴형이 된다.

const CONST_STR = "some_text";
const text = `this is ${CONST_STR}`;
text; // string

const CONST_STR = "some_text";
const text = `this is ${CONST_STR}` as const;
text; // "this is some_text"

 이 변수에는 어떤 값이 들어있었지와 같을 때 타입 정보로 바로 확인되므로 가독성이 높아진다.

 한편으로, 변수를 삽입하면 문자열 리터럴의 Union형이 된다.

let str: "hoge" | "fuga";
const text = `this is ${str}` as const;
text; // "this is hoge" | "this is fuga"

 

 

let의 타입 정의를 스킵하는 기술


 let으로 변수를 선언하는 경우 명확히 타입 정보를 명시할 필요가 있다.

let str: "hoge" | "fuga";
if (bool) {
  str = "hoge";
} else {
  str = "fuga";
}

 위와 같이 특정 조건에 따라 변수가 바로 결정되어 이후에 변경되지 않는 경우는 let이 아닌 const로 선언하여 즉시 실행함수 내에서 조건마다 값을 반환하는 방법으로 하면 타입 정의할 필요가 없어진다.

const str = (() => {
  if (bool) {
    return "hoge";
  } else {
    return "fuga";
  }
})();
str; // "hoge" | "fuga"

 스스로 정의하지 않는 만큼 조건이 늘어났을 때 유연하게 추론된다는 점도 좋다.

const str = (() => {
  if (bool1) {
    return "hoge";
  } else if (bool2) {
    return "fuga";
  } else {
    return "piyo";
  }
})();
str; // "hoge" | "fuga" | "piyo"

 

 

표준 API의 컨스트럭터에 전달하는 인수는 변수로 묶지 않고 직접 기술하기


 어디까지나 경우에 따르지만 표준 API의 컨스트럭터에 전달할 인수는 변수로 묶지 말고 직접 기술하는 편이 여러모로 좋다.

 아래는 WebAPI의 InteresctionObserver 컨스트럭터에 콜백 함수를 전달하고 있는 예이다.

const callback = (entries: IntersectionObserverEntry[]) => {
  // ...
};
const observer = new IntersectionObserver(callback);

표준 API의 인수로 전달한 변수, 함수는 데이터 타입을 일치시켜야할 필요가 있기 때문에 그때 그때 적절하게 명시해야한다. 따라서 기억나지 않을 때마다 매번 다시 검색해야할 필요가 있다.

 한편 변수로 묶지 않고 인수로 넘길 때 기술하면 TypeScript가 자동으로 추론해 주기 때문에 타입을 명시할 필요가 없다.

const observer = new IntersectionObserver((entries) => {
  // entries가 IntersectionObserverEntry[]로 자동 추론된다.
  // ...
});

 에이터에 따라서는 제안해주기 때문에 입력 미스를 막을 수도 있어서 좋다.

 


참고자료

https://qiita.com/ment_RE/items/9387b47dbef6433f6637

728x90