[우아한 타입스크립트] 3장 고급 타입

Jean Young Park·2024년 6월 25일
0

javascript

목록 보기
20/23

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


any 타입

자바스크립트에 존재하는 모든 값을 오류 없이 받을 수 있다.
자바스크립트의 동적 타이핑으로 돌아가는 것과 비슷한 결과를 가져오기 때문에 지양해야 할 패턴으로 알려져 있다. 하지만 어쩔 수 없이 any 타입을 사용해야하는 경우도 있다.

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

unknown 타입

any 타입과 유사하게 모든 타입의 값이 할당될 수 있다. 그러나 any를 제회한 다른 타입으로 선언된 변수에는 unknown 타입 값을 할당할 수 없다.

anyunknown
- 어떤 타입이든 any 타입에 할당 가능
- any 타입은 어떤 타입으로도 할당 가능 (단 never는 제외)- 어떤 타입이든 unknown 타입에 할당 가능
- unknown 타입은 any 타입 외에 다른 타입으로 할당 불가능
let unknownValue: unknown;

unknownValue = 100;
unknownValue = 'hello world';
unknownValue = () => console.log("this is any type");

let someValue1: any = unknownValue;
let someValue2: number = unknownValue; // x
let someValue3: string = unknownValue; // x

void 타입

아무런 값을 반환하지 않는 경우에 사용한다.
주로 함수 반환타입으로 사용되지만 사실 함수에 국한된 타입은 아니고 변수에도 할당 할 수 있지만 함수가 아닌 값에 대해서는 대부분 무의미하다. void 타입으로 지정된 변수는 undefined 또는 null 값만 할당할 수 있다. 하지만 tsconfig.json에서 strictNull-Checks 옵션이 설정되어 있다면 null값을 할당할 수 없다.

let voidValue: void = undefined;

// strictNullCheckes가 비활성화 된 경우에 가능
voidValue = null;

never 타입

값을 반환할 수 없는 타입을 말한다.

  • 에러를 던지는 경우
    런타임에 의도적으로 에러를 방생시키고 캐치할 수 있다.
  • 무한히 함수가 실행되는 경우

Array 타입

자바스크립트에서도 Object.prototype.toString.call(…) 연산자를 사요아여 확인할 수 있다.
자바스크립트에서는 배열을 객체에 속하는 타입으로 분류한다. 즉 자바스크립트에서는 배열을 단독으로 배열이라는 자료형에 국한하지 않는다.
자바스크립트의 배열은 동적 언어의 특징에 따라 어떤 값이든 배열의 원소로 허용한다. 하지만 이런 개념은 타입스크립트의 정적 타이핑과 잘 부합하지 않는다.

const array: number[] = [1, 2, 3];
const array: Array<numbe> = [1, 2, 3];

위 두가지 방식으로 배열 타입을 선언할 수 있으며 차이점은 선언하는 형식 외에는 없다.
Array 키워드 외에도 대괄호([])를 사용해 직접 타입을 명시할 수도 있는데, 이때의 타입은 배열보다 좁은 범위인 튜플을 가리킨다. 튜플은 대괄호 안에 선언하는 타입의 개수가 튜플이 가질 수 있는 원소의 개수를 가진다. 길이까지 제한한다고 볼 수 있다.

let tuple:[number] = [1];
tuple = [1, 2]; // 불가능
tuple = [1, "string"]; // 불가능

enum 타입

열거형이라고도 불리는 구조체를 만드는 타입 시스템이다. 기본적인 추론 방식을 숫자 0부터 1씩 늘려가며 값을 할당하는 것이다.

enum ProgrammingLanguage {
  Typescript, // 0
  Javascript, // 1
  Java, // 2
  Python, // 3
  Kotlin, // 4 
}

ProgrammingLanguage.Kotlin; // 4
ProgrammingLanguage[2] // "Java"

열거형을 사용할 때는 주의해야 할 점도 있는데, 먼저 숫자로만 이루어져 있거나 타입스크립트가 자동으로 추론한 열거형은 안전하지 않은 결과를 낳을 수 있다. 위 예시를 보면 역방향으로도 접근할 수 있음을 보여준다. 이러한 동작을 막기 위해 const enum으로 열거형을 선언하는 방법이 있다.

그러나 const enum으로 열거형을 선언하더라고 숫자 상수로 관리되는 열거형은 선언한 값 이외의 값을 할당하거나 접근할 때 이를 방지하지 못한다. 따라서 문자열 상수 방식으로 열거형을 사용하는 것이 숫자 상수 방식보다 더 안전하며 의도하지 않은 값의 할당이나 접근을 방지하는 데 도움이 된다.

const enum NUMBER {
  ONE = 1,
  TWO = 2,
}

const myNumber: NUMBER = 100; // 에러가 나지 않음
const enum STRING_NUMBER {
  ONE = "ONE",
  TWO = "TWO",
}

const myStringNumber: STRING_NUMBER = "THREE"; // 에러 발생

열거형은 타입스크립트 코드가 자바스크립트로 변환될 때 즉시 실행 함수 형식으로 변환되는 것을 볼 수 있다.

이때 일부 번들러에서 트리쉐이킹 과정 중 즉시 실행 함수로 변환된 값을 사용하지 않는 코드로 인식하지 못하는 경우가 발행할 수 있다. 따라서 불필요한 코드의 크기가 증가하는 결과를 초래할 수 있다. 이를 해결하기 위해서 앞서 언급했던 const enum 또는 as const assertion을 사용해서 유니온 타입으로 열거형과 동일한 효과를 얻는 방법이 있다.

💡 즉시 실행 함수 : 말 그대로 선언과 동시에 실행되는 함수
💡 번들러 : 자바스크립트 파일과 관련된 자산들(HTML, CSS, 이미지 등)을 하나의 파일 또는 몇 개의 파일로 묶어주는 도구
💡 트리쉐이킹 : 모듈 번들링 과정에서 사용되지 않는 코드를 제거하여 최종 번들 크기를 줄이는 최적화 기법

3.2 타입 조합


교차 타입

교차 타입을 사용하면 여러 가지 타입을 결합하여 하나의 단일 타입으로 만들 수 있다. 교차 타입은 &을 사용해서 표기한다.

type ProductItme = {
  id: number;
  name: string;
  type: string;
  price: number;
  imageUrl: string;
  quantity: number;
};

type ProductItemWithDiscount = ProductItem & { discountAmount: number };

유니온 타입

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

type CardItem = {
  id: number;
  name: string;
  type: string;
  imageUrl: string;
};

type PromotionEventItem = ProductItem | CardItem;

const printPromotionItem = (item: PromotionEventItem) => {
  console.log(item.name); // O
  console.log(item.quantity); // 컴파일 에러 발생
}

printPromotionItem 함수를 보면 인자로 PromotionEventItem 타입을 받고 있다 해당 함수 내부에서 quantity를 참조하려고 시도하면 컴파일 에러가 발생하는데, 이는 quantity가 ProductItem에만 존재하기 때문이다.

인덱스 시그니처

특정 타입의 속성 이름은 알 수 없지만 속성값의 타입을 알고 있을 때 사용한다.

interface IndexSignatureEx {
  [key: string]: number;
}

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

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

인덱스 엑세스 타입

다른 타입의 특정 속성이 가지는 타입을 조회하기 위해 사용된다. 인덱스에 사용되는 타입 또한 그 자체로 타입이기 때문에 유니온 타입, keyof, 타입 별칭 등의 표현을 사용할 수 있다.

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

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

const PromotionList = [
  { type: "product", name: "chicken" },
  { type: "product", name: "pizza" },
  { type: "card", name: "cheer-up" },
];

type ElementOf<T> = typeof T[number];

// type PromotinoItemType = { type: string; name: string }
type PromotinoItemType = ElementOf<PromotionList>;

맵드 타입

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

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" };

맵드 타입에서 매핑할 때는 readonly와 ?를 수식어로 적용할 수 있다. 기존 타입에 존재하던 readonly나 ? 앞에 -를 붙여주면 해당 수식어를 제거한 타입을 선언할 수 있다.

type ReadOnlyEx = {
  readonly a:number;
  readonly b: string;
};

type CreateMutable<Type> = {
  -readonly [Property in keyof Type]: Type[Property];
};

type RedultType = CreateMytable<ReadOnlyEx>; // { a: number; b: string }

템플릿 리터럴 타입

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

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

제네릭

정적 언어에서 다양한 타입 간에 재사용성을 높이기 위해 사용하는 문법이다.

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

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

제네릭 함수를 호출할 때 반드시 꺾쇠괄호 안에 타입을 명시해야하는 것은 아니다. 컴파일러가 인수를 보고 타입을 추론해준다.

function exampleFunc<T>(arg: T): T[] {
  return new Array(3).fill(arg);
}

exampleFunc("hellop"); // T는 string으로 추론된다.

제네릭을 사용할 때 주의해야 할 점이 있다. 파일 확장자가 tsx일 때 화살표 함수에 제네릭을 사용하면 에러가 발생한다. 제네릭의 꺾쇠괄호와 태그의 꺾쇠괄호를 혼동하여 생기는 문제이다. 이러한 상황을 피하기 위해서는 제네릭 부분에 extends 키워드를 사용하여 컴파일러에게 특정 타입의 하위 타입만 올 수 있음을 확실히 알려주면 된다.

const arrowExampleFunc = <T>(arg: T): T[] => {
          return new Array(3).fill(arg);
        }

        const arrowExampleFunc2 = <T extends {}>(arg: T): T[] => {
          return new Array(3).fill(arg);
        }

3.2 제네릭 사용법


함수의 제네릭

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

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

호출 시그니처의 제네릭

함수 타입 문법으로 함수의 매개변수와 반환 타입을 미리 선언하는 것을 말한다.

type AddFunction = (a: number, b: number) => number;

const add: AddFunction = (a, b) => {
  return a + b;
};

console.log(add(2, 3)); // 5

제네릭 클래스

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

// 제네릭 클래스 정의
class Box<T> {
  private content: T;

  constructor(content: T) {
    this.content = content;
  }

  getContent(): T {
    return this.content;
  }

  setContent(content: T): void {
    this.content = content;
  }
}

// 제네릭 클래스 인스턴스 생성
const numberBox = new Box<number>(123);
console.log(numberBox.getContent()); // 123

const stringBox = new Box<string>("Hello");
console.log(stringBox.getContent()); // Hello

제한된 제네릭

제한된 제네릭은 타입 매개변수에 대한 제약 조건을 설정하는 기능을 말한다.

type AnimalSound<Animal extends 'cat' | 'dog'> =
Animal extends 'cat' ? 'meow' :
Animal extends 'dog' ? 'woof' :
never;

확장된 제네릭

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

type Trio<T, U, V> = {
  first: T;
  second: U;
  third: V;
};

function printTrio<T, U, V>(trio: Trio<T, U, V>): void {
  console.log(`(${trio.first}, ${trio.second}, ${trio.third})`);
}

// 예시 사용
let trio1: Trio<number, string, boolean> = { first: 1, second: 'hello', third: true };
printTrio(trio1); // 출력: (1, hello, true)

let trio2: Trio<string, Date, number> = { first: 'today', second: new Date(), third: 42 };
printTrio(trio2); // 출력: (today, [current date and time], 42)

제네릭 예시

현업에서 가장 많이 제네릭이 활용될 때는 API 응 답 값의 타입을 지정할 때이다.

  1. 제네릭을 굳이 사용하지 않아도 되는 타입
    첫번째 예시는 두번째 예시와 굳이 GType을 제네릭으로 선언하지 않아도 동일한 결과를 도출한다.
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;
}
  1. any 사용하기
    any를 사용하게 되면 제네릭의 장점인 타입 추론 및 타입 검사를 할 수 있는 이점을 누릴 수 없게 된다.
  2. 가독성을 고려하지 않은 사용

0개의 댓글

관련 채용 정보