Part3_AdvancedType
장경은
책 읽기

[3장] 고급 타입

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

  • 타입스크립트는 자바스크립트 자료형에서 제시되지 않은 독자적인 타입 시스템을 가지고 있다.
  • any, unknown 등은 타입스크립트에만 존재하는 독자적인 타입 시스템이다.
스크린샷 2024-05-04 오후 10 43 50

any 타입

  • 타입을 명시하지 않은 것과 동일한 효과를 나타낸다.
  • any 타입을 변수에 할당하는 것은 지양해야 할 패턴으로 알려져 있다.
  • any 타입을 어쩔 수 없이 사용해야 할 때
    • 개발 단계에서 임시로 값을 지정해야 할 때
    • 어떤 값을 받아올지 또는 넘겨줄지 정할 수 없을 때
    • 값을 예측할 수 없을 때 암묵적으로 사용

unknown 타입

  • unknown 타입은 any 타입과 유사하게 모든 타입의 값이 할당될 수 있다. 그러나 any를 제외한 다른 타입으로 선언된 변수에는 unknown 타입 값을 할당할 수 없다.
// 할당하는 시점에서는 에러가 발생하지 않음
const unknownFunction: unknown = () => console.log(this is unknown type);
 
// 하지만 실행 시에는 에러가 발생; Error: Object is of type unknown.ts (2571)
unknownFunction();
  • any를 보완하기 위한 타입

void 타입

  • 자바스크립트에서는 함수에서 명시적인 반환문을 작성하지 않으면 기본적으로 undefined가 반환된다. 하지만 타입스크립트에서는 void 타입이 사용되는데 이것은 undefined가 아니다. 타입스크립트에서 함수가 어떤 값을 반환하지 않는 경우에는 void를 지정하여 사용한다고 생각하면 된다.
  • 함수 내부에 별도 반환문이 없다면 타입스크립트 컴파일러가 알아서 함수 타입을 void로 추론해주기 때문이다.

never 타입

  • 값을 반환할 수 없는 타입
  • void는 값을 반환하지 않는 타입이지만, never는 값을 반환할 수 없는 타입이다.
  • 값을 반환할 수 없는 경우는 크게 2가지가 있다.
    • 에러를 던지는 경우 throw new Error()
    • 무한히 함수가 실행되는 경우 while(true)

Array 타입

  • 자바스크립트에서는 배열에 숫자, 문자열, 함수 등 다양한 값을 삽입할 수 있다.
    • 하지만 이런 개념은 타입스크립트의 정적 타이핑과 잘 부합하지 않는다.
    • 그러나 필요하다면 유니온 타입을 사용할 수 있다.
    const array1: Array<number | string> = [1, 'string'];
    const array2: number[] | string[] = [1, 'string'];
    const array3: (number | string)[] = [1, 'string'];
  • 튜플도 대괄호를 사용해서 선언할 수 있다.
  • 튜플은 배열의 특정 인덱스에 정해진 타입을 선언한다.
  • 튜플은 길이까지 제한하여 원소 개수와 타입을 보장한다.
let tuple: [number] = [1];
tuple = [1, 2]; // 불가능
tuple = [1, 'string']; // 불가능
 
let tuple: [number, string, boolean] = [1, 'string', true]; // 여러 타입과 혼합도 가능하다
  • 스프레드 연산자를 사용하여 나머지 인덱스를 정할 수도 있다.
const httpStatusFromPaths: [number, string, ...string[]] = [
    400,
    'Bad Request',
    '/users/:id',
    '/users/:userId',
    '/users/:uuid'
];
  • 또한 튜플에서도 물음표 기호와 함께 옵셔널로 사용할 수도 있다.
const optionalTuple1: [number, number, number?] = [1, 2];
const optionalTuple2: [number, number, number?] = [1, 2, 3]; // 3번째 인덱스에 해당하는 숫자형 원소는 있어도 되고 없어도 됨을 의미한다

enum 타입

  • 열거형 이라고도 한다.
  • 앞서 말했듯 타입 공간과 값 공간 모두에서 사용이 가능하다.
enum ProgrammingLanguage {
    Typescript, // 0
    Javascript, // 1
    Java, // 2
    Python, // 3
    Kotlin, // 4
    Rust, // 5
    Go // 6
}
 
// 각 멤버에게 접근하는 방식은 자바스크립트에서 객체의 속성에 접근하는 방식과 동일하다
ProgrammingLanguage.Typescript; // 0
ProgrammingLanguage.Rust; // 5
ProgrammingLanguage['Go']; // 6
 
// 또한 역방향으로도 접근이 가능하다
ProgrammingLanguage[2]; // Java
  • 모든 멤버에 일일이 값을 할당할 수도 있지만, 일부 멤버에 값을 직접 할당하지 않아도 타입스크립트는 누락된 멤버를 아래와 같은 방식으로 이전 멤버 값의 숫자를 기준으로 1씩 늘려가며 자동으로 할당한다.
enum ProgrammingLanguage {
    Typescript = 'TypeScript',
    Javascript = 'JavaScript',
    Java = 300,
    Python = 400,
    Kotlin, // 401
    Rust, // 402
    Go // 403
}
  • const enum으로 선언하면 역방향으로 접근이 불가능하다.
ProgrammingLanguage[200]; // 역방향 접근이 가능하여 undefined를 출력하지만 별다른 에러를 발생시키지 않는다
 
// 다음과 같이 선언하면 위와 같은 문제를 방지할 수 있다
const enum ProgrammingLanguage {}
  • 또한 열거형은 타입스크립트 코드가 자바스크립트로 변환될 때 즉시 실행 함수(IIFE)형식으로 변환되는 것을 볼 수 있다.
    • 이때 일부 번들러에서 트리쉐이킹 과정 중 즉시 실행 함수로 변환된 값을 사용하지 않는 코드로 인식하지 못하는 경우가 발생할 수 있다. 따라서 불필요한 코드의 크기가 증가하는 결과를 초래할 수 있다.
    • 이러한 문제를 해결하기 위해 const enum 또는 as const assertion을 사용할 수 있다.

3.2 타입 조합

교차 타입 (Intersection)

  • 교차 타입을 사용하면 여러 가지 타입을 결합하여 하나의 단일 타입으로 만들 수 있다.
  • 교차 타입은 &을 사용해서 표기한다
type ProductItemWithDiscount = ProductItem & { discountAmount: number };

유니온 타입 (Union)

  • 유니온 타입은 타입 A 또는 타입 B 중 하나가 될 수 있는 타입을 말하며 A | B같이 표기한다.
type PromotionEventItem = ProductItem | CardItem;

인덱스 시그니처 (Index Signatures)

  • 인덱스 시그니처는 특정 타입의 속성 이름은 알 수 없지만 속성값의 타입을 알고 있을 때 사용하는 문법이다.
  • 인터페이스 내부에 [Key: K]: T 꼴로 타입을 명시해주면 되는데 이는 해당 타입의 속성 키는 모두 K 타입이어야 하고 속성값은 모두 T 타입을 가져야 한다는 의미다.
  • 다른 속성을 추가로 명시해줄 수 있는데 이때 추가로 명시된 속성은 인덱스 시그니처에 포함되는 타입이어야 한다.
interface IndexSignatureEx {
    [key: string]: number | boolean;
    length: number;
    isValid: boolean;
    name: string; // 에러 발생
}

인덱스드 엑세스 타입 (Indexed Access Types)

  • 인덱스드 엑세스 타입은 다른 타입의 특정 속성이 가지는 타입을 조회하기 위해 사용된다.
type Example = {
    a: number;
    b: string;
    c: boolean;
};
 
type IndexedAccess = Example[a];
type IndexedAccess2 = Example[a | b]; // number | string
type IndexedAccess3 = Example[keyof Example]; // number | string | boolean
 
type ExAlias = b | c;
type IndexedAccess4 = Example[ExAlias]; // string | boolean

맵드 타입 (Mapped Types)

  • 맵드 타입은 다른 타입을 기반으로 한 타입을 선언할 때 사용하는 문법인데, 인덱스 시그니처 문법을 사용해서 반복적인 타입 선언을 효과적으로 줄일 수 있다.
  • 인덱스 시그니처를 사용하여 반복적인 타입 선언을 효과적으로 줄이는 것을 맵드 타입이라고 하는 듯 하다.
  • 맵드 타입의 특이한 점은 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 }
  • 이를 사용하면 반복 작업을 효율적으로 처리할 수 있다.
// 불필요한 반복이 발생한다
type BottomSheetStore = {
    RECENT_CONTACTS: {
        resolver?: (payload: any) => void;
        args?: any;
        isOpened: boolean;
    };
    CARD_SELECT: {
        resolver?: (payload: any) => void;
        args?: any;
        isOpened: boolean;
    };
    SORT_FILTER: { resolver?: (payload: any) => void; args?: any; isOpened: boolean };
    // ...
};
 
// Mapped Types를 통해 효율적으로 타입을 선언할 수 있다
type BottomSheetStore = {
    [index in BOTTOM_SHEET_ID]: {
        resolver?: (payload: any) => void;
        args?: any;
        isOpened: boolean;
    };
};
  • 맵드 타입에서는 as 키워드를 사용하여 키를 재지정할 수도 있다.
type BottomSheetStore = {
    [index in BOTTOM_SHEET_ID as `${index}_BOTTOM_SHEET`]: {
        resolver?: (payload: any) => void;
        args?: any;
        isOpened: boolean;
    };
};

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

  • 템플릿 리터럴 타입은 자바스크립트의 템플릿 리터럴 문자열을 사용하여 문자열 리터럴 타입을 선언할 수 있는 문법이다.
type StageName = `${stage}-stage`;

제네릭 (Generic)

  • 제네릭Generic은 C나 자바 같은 정적 언어에서 다양한 타입 간에 재사용성을 높이기 위해 사용하는 문법이다.
  • 타입스크립트도 정적 타입을 가지는 언어이기 때문에 제네릭 문법을 지원하고 있다.
  • 함수, 타입, 클래스 등에서 내부적으로 사용할 타입을 미리 정해두지 않고 타입 변수를 사용해서 해당 위치를 비워 둔 다음에, 실제로 그 값을 사용할 때 외부에서 타입 변수 자리에 타입을 지정하여 사용하는 방식을 말한다.
  • any와의 차이는 제네릭을 배열에 사용할 경우 배열 요소가 전부 동일한 타입이라고 보장할 수 있다는 것이다.
  • 제네릭 함수를 호출할 때 타입을 명시하는 부분(<>)을 생략하면 컴파일러가 인수를 보고 타입을 추론해준다.
    • 따라서 타입 추론이 가능한 경우에는 타입 명시를 생략할 수 있다.
  • 함수나 클래스 등의 내부에서 제네릭을 사용할 때 어떤 타입이든 될 수 있다는 개념을 알고 있어야 한다.
    • 특정한 타입에서만 존재하는 멤버를 참조하려고 하면 안된다.
function exampleFunc2<T>(arg: T): number {
    return arg.length; // 에러 발생: Property length does not exist on type T
}
 
// 이럴 때는 제네릭 꺾쇠괄호 내부에 제약을 걸어줌으로써 length 속성을 사용할 수 있게끔 만들 수 있다.
 
interface TypeWithLength {
    length: number;
}
 
function exampleFunc2<T extends TypeWithLength>(arg: T): number {
    return arg.length;
}
  • 제네릭은 파일 확장자가 tsx일 때 화살표 함수에 제네릭을 사용하면 에러가 발생한다.
    • tsx는 타입스크립트 + JSX이므로 제네릭의 꺾쇠괄호와 태그의 꺾쇠괄호를 혼동하여 문제가 생기는 것이다.
    • 보통 이럴 때에는 function 키워드로 선언하는 경우가 많다.

3.3 제네릭 사용법

함수의 제네릭

  • 어떤 함수의 매개변수나 반환 값에 다양한 타입을 넣고 싶을 때 제네릭을 사용할 수 있다.

호출 시그니처의 제네릭

  • 호출 시그니처(call signature)는 타입스크립트의 함수 타입 문법으로 함수의 매개변수와 반환 타입을 미리 선언하는 것을 말한다.
  • 호출 시그니처를 사용할 때 제네릭 타입을 어디에 위치시키는지에 따라 타입의 범위와 제네릭 타입을 언제 구체 타입으로 한정할지를 결정할 수 있다.
  • 제네릭에도 =을 사용해 기본 타입을 정해줄 수 있다.

제네릭 클래스

  • 제네릭 클래스는 외부에서 입력된 타입을 클래스 내부에 적용할 수 있는 클래스이다.
class LocalDB<T> {
    // ...
}
  • 제네릭 클래스를 사용하면 클래스 전체에 걸쳐 타입 매개변수가 적용된다. 특정 메서드만을 대상으로 제네릭을 적용하려면 해당 메서드를 제네릭 메서드로 선언하면 된다.

제한된 제네릭

  • 제한된 제네릭은 타입 매개변수에 대한 제약 조건을 설정하는 기능을 말한다.
  • 예를 들어 string 탕비으로 제약하려면 타입 매개변수는 특정 타입을 상속(extends)해야 한다.
type ErrorRecord<Key extends string> = Exclude<Key, ErrorCodeType> extends never
    ? Partial<Record<Key, boolean>>
    : never;
  • 타입 매개변수가 특정 타입으로 묶였을 때(bind) 키를 바운드 타입 매개변수(bounded type parameters)라고 부른다. 그리고 string을 키의 상한 한계(upper bound)라고 한다.
  • 기본 타입 뿐 아니라 인터페이스나 클래스, 유니온 타입도 상속해서 사용할 수 있다.

확장된 제네릭

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

제네릭 사용의 나쁜 예

  • 제네릭의 장점은 다양한 타입을 받게 함으로써 코드를 효율적으로 재사용할 수 있는 것이다.
  • 하지만 굳이 필요하지 않은 곳에 제네릭을 사용하면 오히려 독이 되어 코드를 복잡하게 만든다.
    • 유니온 타입으로 해결 가능한 경우
    • any를 사용해버리는 경우
    • 가독성을 고려하지 않은 사용