Part3_AdvancedType
이보경
책 읽기

3장. 고급 타입

3.1 타입스크립트만의 독자적 타입 시스템

타입스크립트의 타입 계층 구조

1. any 타입

any 타입을 변수에 할당하는 것은 정적 타입 관점에서 지양해야 할 패턴

tsconfig.json noImplicitAny 옵션 활성화: 타입이 명시되지 않은 변수의 암묵적인 any 타입에 대한 경고 발생

any 타입을 어쩔 수 없이 사용해야 할 때

  • 개발 단계에서 임시로 값을 지정해야 할 때
  • 어떤 값을 받아올지 또는 넘겨줄지 정할 수 없을 때
  • 값을 예측할 수 없을 때 암묵적으로 사용

2. unknown 타입

모든 타입의 값이 할당’될’ 수 있음 any를 제외한 다른 타입의 변수에는 할당’할’ 수 없음

unknown 사용 경우

  • 강제 타입 캐스팅 const env = process.env as unknow as ProcessEnv
  • any 타입 변수에 length를 참조하면 에러가 안나지만 unknown은 발생. 안전하고 엄격

하지만 표 컴포넌트같이 어떤 값을 받을지 모르는 상황에서 unknow을 사용하면, 가공할 때 타입 캐스팅을 모두 해야하는 상황에는 any

3. void 타입

값을 반환하지 않는 경우 변수에 사용할 경우 undefined, null 값만 할당 가능 일반적으로 함수 반환값으로 사용하고, 함수 자체를 다른 함수의 인자로 전달하는 경우에 주로 명시적으로 사용함

4. never 타입

값을 반환할 수 없는 타입 함수 관련 많이 사용되는 타입

  • 에러를 던지는 경우
  • 무한히 함수가 실행되는 경우

5. Array 타입

Array 키워드나 대괄호([])를 이용해 명시 배열 - 타입을 제한 튜플 - 타입과 크기 제한

6. enum 타입

열거형, TS 특수 타입, 구조체를 만드는 타입 시스템 열거형은 각각의 멤버를 가지고 있고 이는 JS의 객체 모양새와 닮았다. 다만 TS는 각 멤버의 값을 스스로 추론한다.

문자열로 지정된 경우와 비교한 장점

  • 타입 안정성
  • 명확한 의미 전달과 높은 응집력
  • 가독성

숫자로만 이루어지거나 자동으로 추론한 열거형은 안전하지 않다.

-> const enum으로 선언하면 역방향으로의 접근을 허용하지 않는다. 하지만 숫자 상수로 관리되는 열거형은 선언 값 이외를 할당하거나 접근하는 것은 방지하지 못한다.

타입 공간뿐만 아니라 값 공간에서도 사용된다.

-> const enum, as const assertioin을 사용해 유니온 타입으로 열거형과 동일한 효과를 얻는 방법 사용하기

3.2 타입 조합

1. 교차 타입 (Intersection type)

A & B

여러 가지 타입을 결합하여 하나의 단일 타입으로 만들 수 있다. 별칭을 붙일 수도 있다.

2. 유니온 타입 (Union type)

A | B

교차 타입이 타입 A와 타입 B를 모두 만족하는 경우라면, 유니온 타입은 타입 A 또는 B 중 하나가 될 수 있는 타입을 말한다.

특정 변수가 가질 수 있는 타입을 전부 나열하는 용도로 사용된다.

3. 인덱스 시그니처 (Index Signatures)

[Key: K]: T

특정 타입의 속성 이름은 알 수 없지만 속성값의 타입을 알고 있을 때 사용하는 문법

타입의 속성 키는 모두 K 타입이어야 하고, 속성값은 모두 T 타입을 가져야 한다는 의미다.

다른 속성을 추가로 명시할때, 추가로 명시된 속성은 인덱스 시그니처에 포함되는 타입이어야 한다.

interface IndexSignatureEx2 {
	[key: string]: number | boolean;
	length: number;
	name: string; // 에러 발생
}

4. 인덱스드 엑세스 타입

다른 타입의 특정 속성이 가지는 타입을 조회하기 위해 사용된다.

type IndexedAccessVar = Example['a' | 'b'] // number | string

또한 배열의 요소 타입을 조회하기 위해 사용하는 경우가 있다.

const PromotionList = [
  { type: "product", name: "chicken" },
  { type: "product", name: "pizza" },
  { type: "card", name: "chee-up" },
];
 
type ElementOf<T> = (typeof T)[number];
 
// type PromotionItemType = { type: string; name: string }
type PromotionItemType = ElementOf<PromotionList>;

5. 맵드 타입 (Mapped Type)

다른 타입을 기반으로 한 타입을 선언할 때 사용하는 문법

인덱스 시그니처 문법을 사용해서 반복적인 타입 선언을 효율적으로 처리할 수 있다.

type Example = {
  a: number;
  b: string;
  c: boolean;
};
 
type Subset<T> = {
  [K in keyof T]?: T[K];
};
 
const aExample: Subset<Example> = { a: 3 };
const bExample: Subset<Example> = { b: "hello" };
const acExample: Subset<Example> = { a: 4, c: true };

매핑할 때는 readonly와 ?를 수식어로 적용할 수 있다.

기존 타입에 존재하던 readonly? 앞에 -를 붙여주면 해당 수식어를 제거한 타입을 선언할 수 있다.

type ReadOnlyEx = {
  readonly a: number;
  readonly b: string;
};
 
type CreateMutable<Type> = {
  -readonly [Property in keyof Type]: Type[Property];
};
 
type ResultType = CreateMutable<ReadOnlyEx>; // { a: number; b: string }
type OptionalEx = {
  a?: number;
  b?: string;
  c: boolean;
};
 
type Concrete<Type> = {
  [Property in keyof Type]-?: Type[Property];
};
 
type ResultType = Concrete<OptionalEx>; // { a: number; b: string; c: boolean }

as 키워드를 사용하여 키를 재지정할 수 있다.

type BottomSheetStore = {
  [index in BOTTOM_SHEET_ID as `${index}_BOTTOM_SHEET`]: {
    resolver?: (payload: any) => void;
    args?: any;
    isOpened: boolean;
  };
};

6. 템플릿 리터럴 타입 (Template Literal Types)

JS의 템플릿 리터럴 문자열을 사용하여 문자열 리터럴 타입을 선언할 수 있는 문법

type Stage =
  | "init"
  | "select-image"
  | "edit-image"
type StageName = `${Stage}-stage`;
// ‘init-stage’ | ‘select-image-stage’ | ‘edit-image-stage’

7. 제네릭 (Generic)

<T>

일반화된 데이터 타입

함수, 타입, 클래스 등에서 내부적으로 사용할 타입을 미리 정해두지 않고 타입 변수를 사용해서 해당 위치를 비워 둔 다음에, 실제로 그 값을 사용할 때 외부에서 타입 변수 자리에 타입을 지정하여 사용하는 방식

보통 타입 변수명으로 T(Type), E(Element), K(Key), V(Value) 등 한 글자로 된 이름을 많이 사용한다.

type ExampleArrayType<T> = T[];
 
const array1: ExampleArrayType<string> = ["치킨", "피자", "우동"];

any의 쓰임과 다르다.

  • any 타입의 배열은 배열 요소들의 타입이 전부 같지 않을 수 있다. 타입 정보를 잃어버린다. 타입 검사하지 않고 모든 타입이 허용되는 타입으로 취급
  • 제네릭은 배열 생성 시점에 원하는 타입으로 특정하는 것 배열 요소가 전부 동일한 타입이라고 보장

제네릭 함수를 호출할 때 타입 추론이 가능한 경우 꺾쇠괄호 안에 타입 명시를 생략할 수 있다.

function exampleFunc<T>(arg: T): T[] {
	return new Array(3).fill(arg);
}
exampleFunc('hello'); // string 타입 추론

또한 요소 타입을 알 수 없을 때는 제네릭 타입에 기본값을 추가할 수 있다.

interface SubmitEvent<T = HTMLElement> extends SyntheticEvent<T> {
  submitter: T;
}

특정한 타입에서만 존재하는 멤버를 참조하려면 제약을 걸어줘야 한다.

interface TypeWithLength {
  length: number;
}
 
function exampleFunc2<T extends TypeWithLength>(arg: T): number {
  return arg.length;
}

주의할 점

.tsx 파일에서 화살표 함수에서 제네릭을 사용하면 에러 발생한다. 제네릭의 꺾쇠괄호와 JSX 문법 태그의 꺾쇠괄호를 혼동하기 때문이다.

따라서 제네릭 부분에 extends 키워드를 사용하여 컴파일러에게 특정 타입의 하위 타입만 올 수 있음을 확실히 알려줘야 한다.

// 에러 발생: JSX element ‘T’ has no corresponding closing tag
const arrowExampleFunc = <T>(arg: T): T[] => {
  return new Array(3).fill(arg);
};
 
// 에러 발생 X
const arrowExampleFunc2 = <T extends {}>(arg: T): T[] => {
  return new Array(3).fill(arg);
};

3.3 제네릭 사용법

1. 함수의 제네릭

어떤 함수의 매개변수나 반환 값에 다양한 타입을 넣고 싶을 때

function ReadOnlyRepository<T>(target: ObjectType<T> | EntitySchema<T> | string): Repository<T> {
  return getConnection(“ro”).getRepository(target);
}

2. 호출 시그니처의 제네릭

| 호출 시그니처: 타입스크립트의 함수 타입 문법, 함수의 매개변수와 반환 타입을 미리 선언하는 것, 함수 호출 시 필요한 타입을 별도로 지정

interface useSelectPaginationProps<T> {
  categoryAtom: RecoilState<number>;
  filterAtom: RecoilState<string[]>; sortAtom:
  RecoilState<SortType>;
  fetcherFunc: (props: CommonListRequest) = > Promise<DefaultResponse<ContentListRes
  ponse<T>>>;
}

3. 제네릭 클래스

외부에서 입력된 타입을 클래스 내부에 적용할 수 있는 클래스

4. 제한된 제네릭

타입 매개변수에 대한 제약 조건을 설정하는 기능

기본 타입, 인터페이스, 클래스 등 타입 매개변수가 특정 타입을 상속하도록 extends 지정

// string 타입으로 제약
type ErrorRecord<Key extends string> = Exclude<Key, ErrorCodeType> extends never
  ? Partial<Record<Key, boolean>>
  : never;

타입 매개변수가 특정 타입으로 묶였을 때, 키를 **바운드 타입 매개변수(bounded type parameters)**라고 부른다. 그리고 string을 키의 **상한 한계(upper bound)**라고 한다.

5. 확장된 제네릭

제네릭 타입은 여러 타입을 상속받을 수 있으며, 타입 매개변수를 여러 개 둘 수도 있다.

// 유연성을 잃은 제네릭
<Key extends string>
 
// 타입 매개변수에 유니온 타입 상속하기
<Key extends string | number>

타입 매개변수가 여러 개 일때

class APIResponse<Ok, Err = string>{ ... }
 
// 사용하는 쪽 코드
const fetChShopStatus = async (): Promise<APIResponse<IShopResponse | null>> => { ...
	return (await API.get<IShopResponse | null>("/v1/main/shop", config)).map(
    (it) => it.result
  );
 }

6. 제네릭 예시

제네릭은 다양한 타입을 받게 함으로써 코드를 효율적으로 재사용할 수 있는 것이 장점 하지만 굳이 필요하지 않은 곳에 사용하면 코드 길이만 늘어나고 가독성을 해칠 수 있다.

가장 많이 활용하는 경우

  • API 응답 값의 타입을 지정할 때

    export interface MobileApiResponse<Data> {
      data: Data;
      statusCode: string;
      statusMessage?: string;
    }
     
    // 사용하는 쪽 코드
    export const fetchPriceInfo = (): Promise<MobileApiResponse<PriceInfo>> => {
      const priceUrl = "https: ~~~"; // url 주소
     
      return request({
        method: "GET",
        url: priceUrl,
      });
    };
     
    export const fetchOrderInfo = (): Promise<MobileApiResponse<Order>> => {
      const orderUrl = "https: ~~~"; // url 주소
     
      return request({
        method: "GET",
        url: orderUrl,
      });
    };

제네릭을 굳이 사용하지 않아도 되는 경우

  • 재사용되지 않고 단순하게 사용될 때

    type GType<T> = T;
    type RequirementType = "USE" | "UN_USE" | "NON_SELECT";
    interface Order {
      getRequirement(): GType<RequirementType>;
    }
     
    // 다음과 동일하다.
    type RequirementType = "USE" | "UN_USE" | "NON_SELECT";
    interface Order {
      getRequirement(): RequirementType;
    }
  • any 사용하기

    type ReturnType<T = any> = {
      // ...
    };

가독성을 고려해 복잡한 제네릭은 의미 단위로 분할해서 사용하기