우아한 타입스크립트 목차

반응형

 

 

 

자료형으로서의 타입 -

변수에 저장할 수 있는 값의 종류는 프로그래밍 언어마다 다르다. 최신 ECMAScript 표준을 따르는 자바스크립트는 7가지 데이터 타입 (자료형)을 정의한다.

undefined, null, Boolean, String, Symbol, Numeric (Number/BigInt), Object

 

타입을 확인하는 방법

타입스크립트에서 typeof, instanceof 그리고 타입 단언을 사용해서 타입을 확인할 수 있다.

typeof 는 연산하기 전에 피연산자의 데이터 타입을 나타내는 문자열을 반환한다.

typeof 연산자가 반환하는 값은 자바스크립트 7가지 기본 데이터 타입(Boolean, null, undefined, Number, BigInt, String, Symbol) 과

Function (함수), 호스트 객체 그리고 Object 객체가 될 수 있다.

typeof 2022; / 'number'

 

 

2.3. 원시타입

 

자바스크립트의 7가지 원시값은 타입스크립트에서 원시타입으로 존재한다.

타입을 파스칼 표기법으로 표기하면 자바스크립트에서는 원시 래퍼 객체라고 한다.

null 과 undefined 를 제외한 모든 원시값은 해당 원시값을 래핑한 객체를 가진다.

타입스크립트에서는 내장원시타입에 해당하는 타입을 파스칼 표기법으로 쓰지 않도록 주의해야한다.

타입스크립트에서도 원시 래퍼 객체가 존재하는데 이것은 고유한 타입으로 분류되기 때문에 둘은 엄연히 다르다.

 

[boolean],

[undefined] - 초기화되어 있지 않거나 존재하지 않음을 나타낸다. 변수 선언후 값 할당하지 않거나 옵셔널일 경우,

[null] - 없다 라는 의미를 명시적으로 나타내는 값,

[number] - NaN, Infinity 포함,

[bigInt] - ES2020 에서 새롭게 도입된 타입으로 타입스크립트 3.2 버전부터 사용할 수 있다. 이전의 자바스크립트는

Number.MAX_SAFE_INTEGER(2^53-1) 를 넘어가는 값을 처리할 수 없었는데 bigInt 를 사용하면 이보다 큰 수를 처리할 수 있다.

number 와 bigInt 타입은 엄연히 다른 타입이기 때문에 상호 작용은 불가능하다.

[string] = `` 로 감싼 문자열 내부에 변수값을 포함할 수 있는 템플릿 리터럴도 포함,

[symbol] - ES2015 에서 도입된 개념으로 Symbol() 함수를 사용하면 어떤값과도 중복되지 않는 유일한 값을 생성할 수 있다.

const MOVIE_TITLE = Symbol('title')

const MUSIC_TITLE = Symbol('title')

console.log(MOVIE_TITLE === MUSIC_TITLE) // false

let SYMBOL: unique symbol = Symbol()

타입스크립트에서는 symbol 타입과 const 선언에서만 사용할 있는 unique symbol 타입이라는 symbol 하위 타입도 있다.

 

 

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

 

any 타입

 

unknown 타입

 

void 타입

 

never 타입

 

Array 타입

 

enum 타입

 

 

3.2. 타입 조합

 

교차타입 (Intersection) (A & B)

 

유니온타입 (Union) (A | B)

 

인덱스 시그니처 (Index Signature)

interface IndexSignatureEx {

  [key: string]: number;

}

 

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

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

// type PromotionItemType = { type: string; name: string }

type PromotionItemType = ElementOf<PromotionList>;

 

 

맵드 타입 (Mapped Types)

type Subset<T> = {

  [K in keyof T]?: T[K];

};

type CreateMutable<Type> = {

  -readonly [Property in keyof Type]: Type[Property];

};

type Concrete<Type> = {

  [Property in keyof type]-?: Type[Property];

};

 

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

type StageName = `${Stage}-stage`;

 

제네릭 (Generic)

T (Type), E (Element), K (Key), V (Value)

type ExampleArrayType<T> = T[];

 

 

3.3. 제네릭 사용법

 

함수의 제네릭

 

호출 시그니처의 제네릭

 

제네릭 클래스

 

제한된 제네릭

타입스크립트에서 제한된 제네릭은 타입 매개변수에 대한 제약조건을 설정하는 기능을 말한다. 타입 매개변수 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) 라고 한다.

상속받을 수 있는 타입으로는 기본타입 뿐만 아니라 상황에 따라 인터페이스나 클래스도 사용할 수 있다. 또한 유니온 타입을 상속해서 선언할 수도 있다.

 

확장된 제네릭

export class APIResponse<Ok, Err = string> {

 

 

4.1. 타입 확장하기

 

extends 와 교차타입

extends 는 교차타입 & 를 사용해서도 동일하게 작성할 수 있다. 유니온타입과 교차타입을 사용한 새로운 타입은 오직 type 키워드로만 선언할 수 있다.

주의할 점은 extends 키워드를 사용한 타입이 교차타입과 100% 상응하지는 않는다는 것이다.

interface 로 할 경우 이미 가진 속성을 다시 선언하면 에러가 발생한다.

 

4.2. 타입좁히기 - 타입가드

 

타입가드는 크게 자바스크립트 연산자를 사용한 타입가드와 사용자 정의 타입가드로 구분할 수 있다.

 

자바스크립트 연산자를 활용한 타입가드는 typeof, instanceof, in 과 같은 연산자를 사용해서 제어문으로 특정 타입 값을 가질 수 밖에 없는 상황을 유도하여

자연스럽게 타입을 좁히는 방식이다. 자바스크립트 연산자를 사용하는 이유는 런타임에 유효한 타입가드를 만들기 위해서이다. 런타임에 유효하다는 말은 타입스크립트

뿐만 아니라 자바스크립트에서도 사용할 수 있는 문법이어야 한다는 의미이다.

 

typeof 연산자는 주로 원시타입을 좁히는 용도로만 사용할 것을 권장한다.

typeof 연산자를 사용하여 검사할 수 있는 타입 목록이다.

string, number, boolean, undefined, object, function, bigint, symbol

 

인스턴스화된 객체 타입을 판별할 때: instanceof 연산자 활용하기

instanceof 는 A 의 프로토타입 체인에 생성자 B 가 존재하는지를 검사해서 존재한다면 true, 그렇지 않다면 false 를 반환한다.

 

객체의 속성이 있는지 없는지에 따른 구분: in 연산자 활용하기

delete 연산자를 사용하여 객체 내부에서 해당 속성을 제거해야만 false 를 반환한다.

자바스크립트의 in 연산자는 런타임의 값만을 검사하지만 타입스크립트에서는 객체 타입에 속성이 존재하는지를 검사한다.

 

is 연산자로 사용자 정의 타입 가드 만들어 활용하기

직접 타입가드 함수를 만들 수도 있다. 이러한 방식의 타입가드는 반환타입이 타입명제 (type predicates) 인 함수를 정의하여 사용할 수 있다.

const isDestinationCode = (x: string): x is DestinationCode => destinationCodeList.includes(x);

if (isDestinationCode(str)) {

  destinationNames.push(DestinationNameSet[str]);

}

 

 

4.3. 타입좁히기 - 식별할 수 있는 유니온 (Discriminated Unions)

 

종종 태그된 유니온 (Tagged Union) 으로도 불리는 식별할 수 있는 유니온 (Discriminated Union) 은 타입 좁히기에 널리 사용되는 방식이다.

식별할 수 있는 유니온을 사용할 때 주의할 점이 있다. 식별할 수 있는 유니온의 판별자는 유닛 타입 (unit type) 으로 선언되어야 정상적으로 동작한다.

유닛 타입은 다른 타입으로 쪼개지지 않고 오직 하나의 정확한 값을 가지는 타입을 말한다. null, undefined, 리터럴 타입을 비롯해 true, 1 등 정확한 값을

나타내는 타입이 유닛 타입에 해당한다. 반면에 다양한 타입을 할당할 수 있는 void, string, number 와 같은 타입은 유닛 타입으로 적용되지 않는다.

공식 깃헙의 이슈 탭을 살펴보면 식별할 수 있는 유니온의 판별자로 사용할 수 있는 타입을 다음과 같이 정의하고 있다.

리터럴 타입이어야 한다.

판별자로 선정한 값에 적어도 하나 이상의 유닛 타입이 포함되어야 하며, 인스턴스화할 수 있는 타입 (instantiable type) 은 포함되지 않아야 한다.

 

 

4.4. Exhaustiveness Checking 으로 정확한 타입 분기 유지하기

 

 

5.1. 조건부 타입

extends, infer, never 등을 활용해 원하는 타입을 만들어보며 어떤 상황에서 조건부 타입이 필요한지 알아보자.

 

extends 와 제네릭을 활용한 조건부 타입

T extends U ? X : Y

조건부 타입에서 extends 를 사용할 때는 자바스크립트 삼항 연산자와 함께 쓴다. 

이 표현은 타입 T 를 U 에 할당할 수 있으면 X 타입, 아니면 Y 타입으로 결정됨을 의미한다.

type PayMethodType<T extends 'card' | 'appcard' | 'bank'> = T extends

  | 'card'

  | 'appcard'

  ? PayMethodInfo<Card>

  : PayMethodInfo<Bank>;

 

infer 를 활용해서 타입 추론하기

삼항 연산자를 사용한 조건문의 형태를 가지는데, extends 로 조건을 서술하고 infer 로 타입을 추론하는 방식을 취한다.

type UnpackPromise<T> = T extends Promise<infer K>[] ? K : any;

UnpackPromise 타입은 제네릭으로 T 를 받아 T 가 Promise 로 래핑된 경우라면 K 를 반환하고, 그렇지 않은 경우에는 any 를 반환한다.

Promise<infer K> 는 Promise 의 반환값을 추론해 해당 값의 타입을 K 로 한다는 의미이다.

const promises = [Promise.resolve('Mark'), Promise.resolve(38)];

type Expected = UnpackPromise<typeof promises>; // string | number

 

type UnpackMenuNames<T extends ReadonlyArray<MenuItem>> =

  T extends ReadonlyArray<infer U> ?

    U extends MainMenu ?

      U['subMenus'] extends infer V ?

        V extends ReadonlyArray<SubMenu> ?

          UnpackMenuNames<V> : U['name'] : never : U extends SubMenu ? U['name'] : never : never;

그 다음 조건에 맞는 값을 추출할 UnpackMenuNames 라는 타입을 추가했다. UnpackMenuNames 는 불변 객체인 MenuItem 배열만 입력으로 받을 수 있도록

제한되어 있으며, infer U 를 사용하여 배열 내부 타입을 추론한다. 코드를 자세히 살펴보면 다음과 같은 동작을 수행한다.

U 가 MainMenu 타입이라면 subMenus 를 infer V 로 추출한다.

subMenus 는 옵셔널한 타입이기 때문에 추출한 V 가 존재한다면 (SubMenu 타입에 할당할 수 있다면) UnpackMenuNames 에 다시 전달한다.

V 가 존재하지 않는다면 MainMenu 의 name 은 권한에 해당하므로 U['name'] 이다.

U 가 MainMenu 가 아니라 SubMenu 에 할당할 수 있다면 (U 는 SubMenu 타입이기 때문에) U['name'] 은 권한에 해당한다.

export type PermissionNames = UnpackMenuNames<typeof menuList>; // '기기 내역 관리' |'헬멧 인증 관리' | '운행 관리'

 

5.2. 템플릿 리터럴 타입 활용하기

 

type Direction =

  | 'top'

  | 'topLeft'

  | 'topRight'

  | 'bottom'

  | 'bottomLeft'

  | 'bottomRight';

이 코드의 Direction 타입은 'top', 'bottom', 'left', 'right' 가 합쳐진 문자열로 선언되어 있다. 이 코드에 템플릿 리터럴 타입을 적용하면

다음과 같이 좀 더 명확하게 표시할 수 있다.

type Vertical = 'top' | 'bottom';

type Horizon = 'left' | 'right';

type Direction = Vertical | `${Vertical}${Capitalize<Horizon>}`;

 

 

5.3. 커스텀 유틸리티 타입 활용하기

 

PickOne 유틸리티 함수

type PickOne<T> = {

  [P in keyof T]: Record<P, T[P]> & Partial<Record<Exclude<keyof T, P>, undefined>>;

}[keyof T];

One<T>

type One<T> = { [P in keyof T]: Record<P, T[P]> }[keyof T];

ExcludeOne<T>

type ExcludeOne<T> = { [P in keyof T]: Partial<Record<Exclude<keyof T, P>, undefined>> }[keyof T];

PickOne<T>

type PickOne<T> = One<T> & ExcludeOne<T>;

type CardOrAccount = PickOne<Card & Account>;

- 식별할 수 있는 유니온으로 객체 타입을 유니온으로 받기를 사용해라 그냥.

 

 

NonNullable 타입 검사 함수를 사용하여 간편하게 타입 가드하기

타입스크립트에서 제공하는 유틸리티 타입으로 제네릭으로 받는 T 가 null 또는 undefined 일 때 never 를 반환하는 타입이다.

type NonNullable<T> = T extends null | undefined ? never : T;

function NonNullable<T>(value: T): value is NonNullable<T> {

  return value !== null && value !== undefined;

}

 

 

5.4. 불변 객체 타입으로 활용하기

 

- 타입스크립트 typeof 연산자로 값을 타입으로 다루기

 

- 객체의 타입을 활용해서 컴포넌트 구현하기

 

type ColorType = keyof typeof theme.colors;

type BackgroundColorType = keyof typeof theme.backgroundColor;

type FontSizeType = keyof typeof theme.fontSize;

interface Props {

  color?: ColorType;

  backgroundColor?: BackgroundColorType;

  fontSize?: FontSizeType;

  onClick: (event: React.MouseEvent<HTMLButtonElement>) => void | Promise<void>;

}

 

 

5.5. Record 원시 타입 키 개선하기

 

무한한 키를 집합으로 가지는 Record

Partial 을 활용하여 정확한 타입 표현하기

type PartialRecord<K extends string, T> = Partial<Record<K, T>>;

이렇게 존재하는 경우에도 타입이 Partial 즉, 옵셔널이기 때문에 항상 ?. 를 붙여서 사용해야 한다. 이것이 키가 무한한 상황에서 PartialRecord 를

사용하는 의도이고 그것이 이러한 상황에 맞는 타입이다. 그냥 Record 사용하면 쪽에서와 같이 런타임 오류를 유발할 있기 때문이다.

 

 

 

반응형

댓글

Designed by JB FACTORY