728x90

2장에선 기본적인 타입과 타입시스템에 대해서 알아봤다면, 3장에서는 자바스크립트 자료형에서 표현되지 않은 자료형에 대해서 설명한다. 존재는 했지만 동적 타이핑 특성 상  표현할 필요성이 없던 자료형들을 타입스크립트에서 정적 타이핑을 하기 위해 정의가 된 애들이다.

 

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

출처 : 우아한 타입스크립트 with React 교재

 

 타입스크립트만의 독자적 타입

1. any  타입

자바스크립트에 존재하느 모든 값을 오류 없이 받을 수 있음.

  • 개발 단계에서 임시로 값을 지정해야 할 때
  • 어떤 값을 받아올지 또는 넘겨줄 지 알 수 없을 때
    ex) 외부라이브러리,웹 API요청에 따라 다양한 값을 반환하는 API, 콜백함수 등등
  • 값을 예측할 수 없을 때 암묵적으로 사용
    ex) 외부라이브러리나 웹 API요청에 따라 다양한 값을 반환하는 API

 

2.  unknown타입

아직 무엇이 할당될 지 모를 때 사용. any타입과 유사한데  비교하면 아래와 같다.

any unknown
-어떤 타입이든 ANY에 할당가능
-any 타입을 어떤 타입으로도 할당 가능(never제외)
-어떤 타입이든 unknown 타입에 할당 가능
-unknown 타입은 any타입 외에 다른 타입으로 할당 불가능

 

any 와 추가적인 차이점은 할당하는 시점에서는 에러가 발생하지 않으나, 실행시에 에러가 발생한다.

즉, 어떠한 타입이든 가져올 수 이찌만 개발자에게 엄격한 타입검사를 강제한다.

 

배민팀에서는 강제 타입 캐스팅을 할 때 쓰거나, 에러가 발생한다는 점 덕에 any보다 선호한다고 했다.

 

3.  void 타입

함수에서 반환 결과가 없는 경우를 말한다.

자바스크립트에서는 반환하는 값이 없는 함수의 경우 undefined 가 반환되는데, 타입스크립트에서 void는 undefined는 아니다.

void에는 null과 undefined가 할당가능하지만 ,strictNullChecks를 세팅할 경우 에러가 뜨고 추천되지 않는 방식이다.

 

4.  never 타입

함수와 관련해서 쓰는데, 값을 반환할 수 없는 경우 ( 반환 결과가 없는 것과 다름)

크게 2가지 경우로 나뉘는데

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

5.  Array 타입

자바스크립트에서도 Object.prototype.toString.call(..) 연사자로 확인할 수 있는데 이 함수는 객체의 인스턴스까지 알려주기 때문에 나오는 것이다.

var arr = [];
console.log(Object.prototype.toString.call(arr)); //[object Array]

자바스크립트에서 존재하는 타입이라 생각할 수 있는데 구분하는 이유는 아래와 같다.

 

  • 자바스크립트에서는 배열을 객체에 속하는 타입으로 분류함
  • 타입스크립트에서 Array라는 타입을 사용하기 위해서는 타입스크립트의 특수한 문법을 다뤄야 함

 

타입 제한하기

 

자바스크립트에서는 배열안에 들어갈 값에 제한이 없다.( 다양한 값이 들어갈 수 있음)

하지만 이러한 성격은 정적 타이핑과 잘 부합하지 않고, 실제로 다른 언어는 배열의 원소로 하나의 타입만 허용한다.

이를 제한하기 위해서 자료형+ [] 형식으로 배열 타입을 선언할 수 있고, 여러가지 선언할 경우 유니온 타입을 사용할 수 있다.

const arr = [];
console.log(Object.prototype.toString.call(arr));

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

const unionArr: Array<number | string> = [1, 'string'];
const unionArr2: (number | string)[] = [1, 'string'];

 

길이 제한하기

자바등 다른 정적 타입의 경우 길이도 제한하는 경우가 많다. 이때 사용하는 것이 튜플 타입이다.

대괄호 안에 들어가는 원수의 수가 튜플이 가질 수 있는 원소의 개수다

const tuple: [number] = [1];
const tuple2: [number, string] = [1, 'string'];

 

가장 대표적인 예시는 react의 useState의 경우이다.  

const [username, setUsername] = useState('');

 

이렇게 튜플 타입을 사용할 경우 객체의 구조 분해 할당을 사용하는 것과 다르게 변수 이름에서 자유로워진다.

유연성이 향상된다.

 

6  enum 타입

열거형이라고도 부르는 특수한 타입이다. 

일일이 값을 할당할 수도 있지만  ,누락된 번호의 경우에는 이전 멤버 값의 숫자를 1씩 늘리며 자동 할당한다.

enum Language {
  Typescript, //0
  Javascript, //1
  Java, //2
  Python = 'Python',
  Kotlin = 300,
  Rust, //301
}

 

열거형은 그 자체로 변수 타입으로 지정할 수 있다. 따라서 코드 가독성을 높인다.

 

enum ItemStatusType {
  HOLd = 'HOLD', //배송보류
  READY = 'READY', //배송 준비
  DELIVERING = 'DELIVERING', // 배송 중
  DELIVERED = 'DELIVERED', // 배송완료
}

const checkItemAvailable = (itemStatus: ItemStatusType) => {
  switch (itemStatus) {
    case ItemStatusType.HOLd:
    case ItemStatusType.READY:
    case ItemStatusType.DELIVERING:
      return false;
    case ItemStatusType.DELIVERED:
      defalut: return true;
  }
};

 

위의 예시와 같이 얻을 수 있는 효과로는

  • 타입 안정성 (명시되지 않은 값은 받을 수 없다)
  • 명확한 의미 전달과 높은 응집력 : 타입이 어떤 값을 다루는 지 명확하고, 아이템 상태에 대한 값을 모아 놓아서 응집력이 높다
  • 가독성 : 응집도가 높기 떄문에 말하고자 하는 바가 명확하다.

 

다만 열거형의 경우에는 범위를 넘어서는 경우 접근해도 에러가 생기지 않는다. const enum으로 열거형 선언을 할 경우 역방향으로의 접근을 허용하지는 않지만, 문자열로 사용하는 것이 더 안전하다.

(교제에서는 에러는 발생하지 않는다고 하는데, 에러가 생김,,)

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

enum NUM {
  ONE = 1,
  TWO = 2,
  THREE,
}

const notMyNumber: NUM = 100; //'100' 형식은 'NUMBER' 형식에 할당할 수 없습니다.ts(2322)

const myNumber: NUMBER = 100; //'100' 형식은 'NUMBER' 형식에 할당할 수 없습니다.ts(2322)

const enum STRING_NUMBER {
  ONE = 'ONE',
  TWO = 'TWO',
}

const myStringNum: STRING_NUMBER = 'THREE'; //'"THREE"' 형식은 'STRING_NUMBER' 형식에 할당할 수 없습니다.ts(2322)

 

또한 추가적인 문제점은 열거형은 타입스크립트 코드가 자바스크립트 코드로 변환되어 트리쉐이킹 과정에서 사용하지 않는 코드로 인식하지 못하는 경우가 발생할 수 있다.

(function (NUM) {
    NUM[NUM["ONE"] = 1] = "ONE";
    NUM[NUM["TWO"] = 2] = "TWO";
    NUM[NUM["THREE"] = 3] = "THREE";
})(NUM || (NUM = {}));
var notMyNumber = 100; //'100' 형식은 'NUMBER' 형식에 할당할 수 없습니다.ts(2322)
var myNumber = 100; //'100' 형식은 'NUMBER' 형식에 할당할 수 없습니다.ts(2322)
var myStringNum = 'THREE'; //'"THREE"' 형식은 'STRING_NUMBER' 형식에 할당할 수 없습니다.ts(2322)

 

 타입 조합

 

1.  교차 타입

여러 가지 타입을 결합하여 하나의 단일 타입을 만들 수 있다

A & B는 A와 B 모두를 만족하는 타입이다.  

type ProductItem = {
  id: number;
  name: string;
  type: string;
  price: number;
};

type ProductItemWithDiscount = ProductItem & { discountAmount: number };

const item: ProductItem = { id: 1, name: '굽네', type: '치킨', price: 20000 };

const disItem: ProductItemWithDiscount = {
  id: 1,
  name: '굽네',
  type: '치킨',
  price: 20000,
  discountAmount: 500,
};

 

2.  유니온 타입

A와 B중 하나를 만족하는 타입. 그렇기 때문에 둘 중 하나만 해당하는 경우에는 컴파일 에러가 뜬다.

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

type PromotionEventItem = ProductItem | CardItem;

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

 

3.  인덱스 시그니처

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

[Key:K ] :T 꼴로 타입을 명시해주면 된다.

타입을 추가로 명시할 수 있는데 이 경우에는 인덱스 시그니처에 포함되어야 한다.

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

 

4.  인덱시드 엑세스 타입

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

type Example = {
  a: number;
  b: string;
  c: boolean;
};

type IndexedAccess = Example['a'];
type IndexedAccess2 = Example['a' | 'b'];
type IndexedAccess3 = Example[keyof Example];

const indexedConst: IndexedAccess = 3;
const indexedConst2: IndexedAccess2 = 3;
const indexedConst2n1: IndexedAccess2 = '3';
const indexedKeyOf1: IndexedAccess3 = '3';
const indexedKeyOf2: IndexedAccess3 = 3;
const indexedKeyOf3: IndexedAccess3 = false;

 

배열의 타입을 조회할 때도 쓸 수 있다.

 

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

const PromotionList2 = [
  { type: 'product', name: 'chicken' },
  { type: 'product', name: 'pizza' },
  { type: 3, name: 'cheer-up' },
];

type ElementOf = (typeof PromotionList)[number];
const promotionData: ElementOf = { type: 'product', name: 'chicken' };
type ElementOf2 = (typeof PromotionList2)[number]; //type ElementOf2 = {  type: string; name: string;} | { type: number; name: string;}

5.  맵드 타입

map은 보통 유사한 형태를 가진 여러 항목의 목록 A를 변환된 항목의 목록 B로 바꾸는 것을 의미한다.

type MapExample = {
  a: number;
  b: string;
  c: boolean;
};

type Subset<T> = {
  [K in keyof T]?: T[K];
};


const aExample: Subset<MapExample> = { a: 3 };
const aExample2: Subset<MapExample> = { b: 'string' };
const aExample3: Subset<MapExample> = { a: 3, b: 'string' };

a,b,c가 필수 이던 타입을 아래와 같이 optional 하게 바꿀 수도 있고,

-readonly  -? 를 통해서 readonly 와 옵셔널을 제거할 수도 있다. 불필요한 반복을 줄이기 위해 사용한다.

 

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

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

type ResultType = CreateMuatable<ReadOnlyEx>; //{ a: number; b: string;}

 

6.  템플릿 리터럴 타입

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

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

 

새로운 유니온 타입을 만들 수 있다.

 

7.  제네릭

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

제네릭안에는 어떠한 타입이든 올 수 있지만, 생성 시점에 원하는 타입을 특정할 수 있다.

아래 예시는 string만 올 수 있는 배열을 제네릭 문법을 통해 생성하였다.

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

 

타입  추론이 가능한 경우에는 타입 명시를 생략할 수 있다.

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

function exampleFunc2<T>(arg: T): number {
  return arg.length; // 에러가 생김 'T' 형식에 'length' 속성이 없습니다
}

 

이때 어떤 타입이든 올 수 있기에 특정 타입에만 존재하는 메서드를 제네릭을 사용할 떄 함꼐 쓰면 안된다.

또한 화살표함수에서 JSX문법에서의 꺽쇠괄호와 혼동이 생길 수 있으므로 해당 경우에는 extends 키워드를 사용하여야 한다.

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

 

 

제네릭의 사용처는 다양하다

  1. 함수의 제네릭
    함수의 매개변수나 반환값에 다양한 타입을 넣고 싶을 때
  2.  호출 시그니처의 제네릭
    호출 시그니처란 타입스크립트의 함수 타입 문법으로 함수의 매개변수와 반환 타입을 미리 선언하는 것을 말한다.
    // 배민선물하기 예시
    interface useSelectPaginationProps<T> {
      categoryAtom: RecoilState<number>;
      filterAtom: RecoilState<string[]>;
      sortAtom: RecoilState<SortType>;
      fetcherFunc: (
        props: CommonListRequest
      ) => Promis<DefaultResponse<ContentListResponse<T>>>;
    }​
  3. 제네릭 클래스
    외부에서 입력된 타입을 클래스 내부에 적용할 수 있는 클래스
    클래스 이름 뒤에 타입 매개변수인 <T>를 선언해준다.
    class GenericNumber<NumType> {
      zeroValue: NumType;
      add: (x: NumType, y: NumType) => NumType;
    }
     
    let myGenericNumber = new GenericNumber<number>();
    myGenericNumber.zeroValue = 0;
    myGenericNumber.add = function (x, y) {
      return x + y;
    };​


  4. 제한된 제네릭
    타입 매개변수에 대한 제약 조건을 설정하는 기능을 말한다.
    유니온 타입을 상속해서 유연성을 잃지 않으면서 타입 제약을 할 수 있다. 
    아래에는 extends로 제한하여서 length 메서드가 가능하게 하였다.
    interface Lengthwise {
      length: number;
    }
    
    function loggingIdentity<Type extends Lengthwise | string>(arg: Type): Type {
      console.log(arg.length);
      return arg;
    }​
     

 

제네릭 사용 유의사항
제네릭의 장점을 다양한 타입을 받을 수 있어 코드를 효율적으로  재사용할 수 있다는 점이다. 그러다보니 API 응답 값의 타입을 지정할 때 제네릭을 많이 쓰게 된다.

다만 유의할 점은 3가지다

  • 제네릭을 굳이 사용하지 않아도 되는 타입
    type GType<T> = T;​
  • any 사용하기
    마찬가지로 제네릭과 any를 사용한다면 자바스크립트를 쓰는 것과차이가 없다.
  • 가독성을 고려하지 않기
    너무 남용할 경우 남이 알아보기 힘들다.

 

 

 

 

+ Recent posts