[TIL-0515] 제네릭, 타입 단언

jiny·2025년 6월 2일

캡스톤2

목록 보기
16/22

🌟 도시 기본 정보 추출하기

메인 페이지의 도시 카드 모달에서 정적 데이터로 저장해 놓은 도시 기본 정보(항공, 시차, 비자, 여권 만료 조건, 전압)를 보여주기 위해서 타입을 정의하고 함수를 생성하였다.

  • countryBasicInfo.ts : 도시 기본 정보 정적 데이터

    export const COUNTRY_BASIC_INFO: CountryCityInfo = {
      대한민국: {
       항공: "없음",
       시차: "없음",
       비자: "없음",
       여권만료조건: "없음",
       전압: "220V 60Hz / Type C, F",
       변환: "불필요",
     },
     일본: {
       도쿄: {
         항공: "직항 2시간 05분 ~ 2시간 25분",
         시차: "없음 (UTC+9)",
         비자: "90일 무비자 체류 가능",
         여권만료조건: "일본 체류 기간 동안 유효",
         전압: "100V 50Hz / Type A, B",
         변환: "필수",
       },
       삿포로: {
         항공: "직항 2시간 30분 ~ 3시간 00분",
         시차: "없음 (UTC+9)",
         비자: "90일 무비자 체류 가능",
         여권만료조건: "일본 체류 기간 동안 유효",
         전압: "100V 50Hz / Type A, B",
         변환: "필수",
       },
       오사카: {
         항공: "직항 1시간 45분 ~ 2시간 05분",
         시차: "없음 (UTC+9)",
         비자: "90일 무비자 체류 가능",
         여권만료조건: "일본 체류 기간 동안 유효",
         전압: "100V 60Hz / Type A, B",
         변환: "필수",
       },
       후쿠오카: {
         항공: "직항 1시간 20분 ~ 1시간 40분",
         시차: "없음 (UTC+9)",
         비자: "90일 무비자 체류 가능",
         여권만료조건: "일본 체류 기간 동안 유효",
         전압: "100V 60Hz / Type A, B",
         변환: "필수",
       },
     },
     // 내용 생략
    };
  • cityInfo.type.ts

     // 공통 정보 타입
     export type CityInfo = {
       항공: string;
       시차: string;
       비자: string;
       여권만료조건: string;
       전압: string;
       변환: "필수" | "선택" | "불필요";
     };
    
     export type NestedCityInfo = {
       [cityName: string]: CityInfo;
     };
    
     // 전체 국가 정보 타입
     export type CountryCityInfo = {
       [countryName: string]: CityInfo | NestedCityInfo;
     };
  • getCityInfo.ts : 도시 이름과 키를 기반으로 해당 도시 기본 정보를 안전하게 추출하는 함수

    export const getCityInfo = <K extends keyof CityInfo>(
      city: string, // 조회할 도시 이름 (예: "서울", "로마")
      countryData: CityInfo | NestedCityInfo, // 도시 정보를 담고 있는 객체 (단일 계층 또는 중첩 구조)
      key: K // 조회할 정보의 키 (예: "항공", "비자")
    ): CityInfo[K] | null => { // 반환 타입은 CityInfo의 키에 해당하는 값 또는 null
      
      // countryData가 객체이고, "항공"이라는 키가 없다면 => NestedCityInfo 구조로 간주함
      if (typeof countryData === "object" && !("항공" in countryData)) {
        // city가 존재하고, countryData에 해당 도시(city)의 정보가 있을 경우,
        // 해당 도시의 key에 해당하는 정보를 반환함
        if (city && countryData[city]) return countryData[city][key];
        else return null; // 도시 정보가 없으면 null을 반환함
      }
      
      // "비자"라는 키가 존재한다면 => 단일 CityInfo 구조로 간주함
      // 단일 CityInfo로 간주하고 key 값을 반환함
      if ("비자" in countryData) return (countryData as CityInfo)[key];
      
      return null; // 위 조건에 모두 해당하지 않으면 null을 반환함
    }

위 함수에서 제네릭(Generic), 타입 가드, 타입 단언 등 다양한 타입스크립트의 개념이 나오는데, 함수에 등장하는 타입스크립트 개념들에 대해서 정리해보기로 했다.


🌟 <K extends keyof CityInfo>

타입스크립트에서 <K extends keyof CityInfo>제네릭(Generic)을 사용할 때 타입을 제한하는 방법 중 하나이다.

  1. <K>는 제네릭 타입 파라미터

    제네릭은 재사용 가능한 유연한 함수(또는 타입)를 정의할 수 있게 해준다.

    function identity<T>(arg: T): T {
      return arg;
    }

    이처럼 T가 들어간 곳에 어떤 타입이든 넣을 수 있다.

  2. keyof CityInfo

    이건 CityInfo 타입의 key들을 union type으로 추출하는 TypeScript 연산자이다.
    따라서 keyof CityInfo는 다음과 같은 유니언 타입이 된다.

    "항공" | "시차" | "비자" | "여권만료조건" | "전압" | "변환"
  3. <K extends keyof CityInfo>

    이제 이 전체는 "K는 CityInfo의 key 중 하나여야 한다"는 의미이다.
    즉, K"항공", "시차", "비자", "여권만료조건", "전압", "변환" 중 하나가 될 수 있다는 제한을 건 것이다.

    • 효과: 이렇게 하면 key 파라미터가 CityInfo에 존재하는 key가 아닌 값이 들어오는 걸 타입스크립트가 막아줘서 타입 안정성이 높아진다.
  4. CityInfo[K]

    CityInfo[K]인덱스드 액세스 타입(indexed access type)이다.

    예를 들어,

    • K = "항공"이면 CityInfo[K]는 string 타입
    • K = "변환"이면 CityInfo[K]는 유니언 타입

    즉, key에 해당하는 값의 타입을 자동으로 추론해서 반환 타입으로 사용한다.

  5. 최종 요약

    export const getCityInfo = <K extends keyof CityInfo>(
      city: string,
      countryData: CityInfo | NestedCityInfo,
      key: K
    ): CityInfo[K] | null
    • KCityInfo의 key 중 하나여야 함
    • 반환 타입은 CityInfo[K], 즉 그 키에 해당하는 타입(string, union)임
    • 타입 안전하게 동작해서 key를 잘못 넣으면 컴파일 시점에 에러 발생

🌟 "비자" in countryData

"비자" in countryData는 JavaScript/TypeScript의 in 연산자를 사용한 것으로, 해당 객체(countryData)에 "비자"라는 속성(key)이 존재하는지를 확인하는 조건문이다.

  1. 기본 문법

    "key" in object
    • "key" : 문자열 형태의 속성 이름
    • object : 검사할 대상 객체
    • 결과: true 또는 false 반환
  2. "비자" in countryData는 무슨 뜻일까?

    if ("비자" in countryData) {
      // countryData는 CityInfo라고 간주해도 안전함
    }
    • countryData"비자"라는 속성을 직접 가지고 있다면 true를 반환한다.
    • 즉, countryData가 다음과 같이 생겼다면,
      {
        항공: "직항 1시간 45분 ~ 2시간 00분",
        시차: "한국 기준 -1시간 (UTC+8)",
        비자: "30일 무비자 체류 가능",
        여권만료조건: "중국 도착 시점 기준 유효",
        전압: "220V 50Hz / Type A, C, I",
        변환: "선택",
      }
      이 객체에는 "비자"라는 key가 있으므로 true이다.
  3. 왜 이렇게 쓸까?

    이 코드는 CityInfoNestedCityInfo를 구분하기 위해 사용되었다.

    • CityInfo : 도시별 여행 정보를 담은 평평한 구조
    • NestedCityInfo : 도시 이름을 key로 하고 그 값이 CityInfo인 중첩 구조
    export type CityInfo = {
       항공: string;
       시차: string;
       비자: string;
       여권만료조건: string;
       전압: string;
       변환: "필수" | "선택" | "불필요";
     };
    
     export type NestedCityInfo = {
       [cityName: string]: CityInfo;
     };
    • "비자"라는 key가 있으면 CityInfo라고 판단할 수 있음
    • 반대로 "비자"가 없다면 NestedCityInfo로 간주할 수 있음
    • "비자" 대신 "항공", "시차", "여권만료조건"CityInfo에 존재하는 어떤 속성을 사용해도 동작은 같음 (단지 "비자"가 대표적인 필드로 선택된 것임)
  4. 최종 정리

    "비자" in countryData

    • countryData 객체가 "비자"라는 속성을 가지고 있는지 검사하는 것
    • 이 조건을 통해 CityInfo 타입인지(NestedCityInfo가 아닌지) 구분할 수 있음
    • 타입 가드 역할도 하기 때문에, 그 블록 안에서는 countryData as CityInfo처럼 안전하게 접근 가능

🌟 countryData as CityInfo

countryData as CityInfo는 타입스크립트의 타입 단언(Type Assertion) 문법이다.

  1. 타입 단언(Type Assertion)이란?

    타입 단언은 타입스크립트에게 "개발자인 내가 이 값의 타입을 정확히 알고 있으니, 이 타입으로 취급해줘!"라고 명시적으로 알려주는 것이다.

    • 문법
      value as TargetType
    • 예시
      const value = "123" as string;
  2. as를 써야 할까?

    countryData는 다음처럼 두 가지 타입 중 하나로 선언되어 있다.

    countryData: CityInfo | NestedCityInfo

    이 상태에서 countryData["비자"]처럼 바로 접근하려고 하면, TypeScript는 타입이 아직 확정되지 않았다고 판단안전하게 접근할 수 없다고 막는다.

    그래서 개발자가 직접 타입을 확정해주는 것이다.

    (countryData as CityInfo)["비자"]

    ➡️ "이 시점에서는 countryData가 CityInfo라는 걸 내가 확실히 알고 있다"고 단언하는 것이다.

  3. 타입 단언은 캐스팅(casting)과 같을까?

    • 타입 단언 : 타입스크립트에서 컴파일러에게만 지정한 타입으로 취급해달라고 명령하는 것
    • 타입 캐스팅 : 하나의 타입을 다른 타입으로 변환하는 것

    비슷하지만 약간 다르다.
    런타임에서는 아무 영향도 없고, 오직 컴파일 시점의 타입 검사만 달라진다.

    다음과 같이 말도 안 되는 단언도 컴파일은 통과된다.

    const num = 123 as unknown as string;

    ➡️ 즉, 개발자가 책임지고 올바르게 사용해야 하는 도구이다.

  4. 이 코드에서는 왜 꼭 필요할까?

    이 코드에서는 countryDataCityInfo인지, NestedCityInfo인지에 따라 타입이 다르다.

    그래서 아래처럼 in 연산자를 먼저 사용해 타입을 좁히고,

    if ("비자" in countryData) {
      // 이 안에서는 사실상 CityInfo 타입임

    그다음 안전하게 값을 꺼내려면 TypeScript한테

    (countryData as CityInfo)[key]

    라고 명시적으로 CityInfo로 다룰 수 있게 허락해주는 것이다.


🌟 if("비자" in countryData) return (countryData as CityInfo)[key]

  1. "비자" in countryData

    이건 countryData 객체에 "비자"라는 key가 존재하는지 확인하는 조건이다.

    • countryDataCityInfo | NestedCityInfo라는 유니언 타입인데, 이 중에서 "비자"라는 key는 CityInfo에만 있다.
    • 즉, "비자" in countryData 조건이 참이면 countryDataCityInfo라고 간주할 수 있다.
  2. (countryData as CityInfo)[key]

    이건 타입 단언(Type Assertion)이라고 한다.

    • countryData는 현재 CityInfo | NestedCityInfo 타입이다.
    • 그런데 if 문 안에서는 "비자"가 있으니까 CityInfo로 좁혀졌다고 생각한다.
    • 하지만 TypeScript는 그걸 자동으로 확신하지 못할 수도 있어서, 명시적으로 as CityInfo라고 써줘야 에러가 안 난다.
  3. 전체 해석

    if ("비자" in countryData)
      return (countryData as CityInfo)[key];

    ➡️ "비자"라는 key가 존재하면, countryDataCityInfo 타입이 확실하니까 CityInfo로 단언한 후, key에 해당하는 값을 꺼내서 반환하라.

0개의 댓글