Post

[ts] 점진적으로 타입스크립트를 적용해 나가는 프로세스 및 프로젝트 환경 구성, 편리한 유지보수를 위한 유틸리티 타입

타입스크립트를 점진적으로 적용해 나가기

⚔️ 타입스크립트를 점진적으로 적용해 나가기
1
2
3
4
5
(1) 타입스크립트 환경 구성
(2) 명시적인 any 선언
(3) 구체적인 타입 정의
(4) 외부 라이브러리 모듈화
(5) 'strict' 옵션 추가 후 타입 정의

타입스크립트 환경 구성

🥯 타입스크립트 환경 구성

typescript 라이브러리를 설치하고 관리하기 위해 우선, npm 초기화부터 수행하여,
🧆 package.json 파일을 생성한다.
※ package.json에 커스텀 명령어 “build”: “tsc” 지정

🍍 package.json
nodejs에서 사용하는 모듈들을 패키지로 만들어 관리하고 배포하는 역할을 한다.

typescript 라이브러리를 개발용으로 설치한다.

1
2
npm init -y
npm i -D typescript

타입스크립트 설정 파일 🎳 tsconfig.json 을 생성한다.

1
2
3
4
5
6
7
8
9
10
{
  "compilerOptions": {
    "allowJs": true,
    "target": "ES5",
    "outDir": "./dist",
    "moduleResolution": "node",
    "lib": ["ES2015", "DOM", "DOM.Iterable"]
  },
  "include": ["./src/**/*"]
}
🏴 "allowJs": true
js까지 ts가 검증
ts를 점진적으로 적용하기 위해 js까지 컴파일한다.
🏴 "outDir": "./dist"
ts의 결과물이 어디에 들어갈 것인지를 지정
🏴 "moduleResolution": "node"
Promise를 인식시켜 주기 위해 설정
🏴 "include": ["./src/**/*"]
어떤 파일을 대상으로 ts파일을 컴파일 할 것인지 지정
["**/*"], default 모든 폴더 밑에 있는 모든 파일을 대상으로
🏴 "exclude": ["node_modules", "bower_components", "json_packages"]
어떤 파일을 ts 컴파일에서 제외 할 것인지 지정, default

해당 환경을 구성한 뒤에,
js파일을 하나씩 ts파일로 변환하고 빌드해본다.

1
npm run build

타입에러가 났음에도 js파일이 생성된다.
즉, 타입에러와 런타임에러는 서로 독립적인 관계이다.

명시적인 any 선언

🥯 명시적인 any 선언

🎳 tsconfig.json 파일에 noImplicitAny: true 옵션을 추가한다.
우선 에러를 해결한다는 관점에서만 에러가 난 부분에 명시적으로 any타입을 정의해준다.

※ 타입을 정하기 어려운 곳이 있으면 명시적으로 any롤 선언한다.

외부 라이브러리 모듈화 axios, chart.js

🥯 외부 라이브러리 모듈화 axios, chart.js
🍍 axios
Promise based HTTP client for the browser and node.js
npm i axios
🍪 타입스크립트가 외부 라이브러리(모듈)를 해석하는 방식
js 라이브러리를 ts에 이식하려면 ts가 인식할 수 있게 중간에 타입을 정의해줘야 한다.
즉, 타입 정의 파일 (index.d.ts) 이 있어야 한다. d: declaration type, 선언파일
  • (방법1) 라이브러리 내부 자체에 있는 경우, 해당 폴더 하위의 index.d.ts 파일 참조
  • (방법2) 타입 정의 라이브러리가 따로 있는 경우, 해당 라이브러리를 설치 in Definitely Typed
    node_modules 아래 @types 하위 폴더에 정의된 index.d.ts 파일 참조
  • (방법3) 타입 정의 라이브러리가 제공되지 않는 경우, index.d.ts 파일 직접 정의
    🎳 tsconfig.json"typeRoot": ["./node_modules/@types", "./types"] 지정
    ./types 하위에 라이브러리명으로 폴더를 만들고 index.d.ts파일 정의 ex. ./types/chart.js/index.d.ts

    1
    
    declare module 'chart.js';
    

    ※ 옛 버전의 chart.js에는 라이브러리 내부 자체 index.d.ts 파일이 없었다.

  • (방법4) vue3의 DefineComponent 이용
    ex. type QuillEditorType = DefineComponent<typeof QuillEditor>;
    DefineComponentvue 컴포넌트의 타입을 명확히 지정할 때 사용된다.
    typeof QuillEditor는 QuillEditor라는 외부 API 객체의 런타임 타입을 가져온다.
    즉, 해당 런타임 타입을 가져와 vue 컴포넌트의 타입을 명확하게 지정하여 quillEditor의 타입을 지정할 수 있다.
📘 Definitely Typed
The repository for high quality
Typescript type definitions.
외부 라이브러리에 대해 이미 만들어논 오픈소스 타입 정의 라이브러리 저장소

라이브러리 내부 자체에 타입정의 파일 index.d.ts이 없는 경우
Definitely Typed 저장소의 @types에서 관련 타입정의 라이브러리를 찾아 설치한다.
@types : Definitely Typed 저장소 하위에 정의된 타이핑(typing) 라이브러리

strict 옵션 추가 후 타입 정의

🥯 strict 옵션 추가 후 타입 정의

🎳 tsconfig.json 파일에 "strict": true 옵션 추가
좀 더 엄격하게 타입을 정의할 수 있게 동작을 점검한다.
🎯 추후에 일어날 수 있는 타입정의에 대한 오류를 막을 수 있다.

해당 옵션을 추가하면 아래의 옵션들을 추가한 효과가 생긴다.

1
2
3
4
5
6
"alwaysStrict": true
"strictNullChecks": true
"strictBindCallApply": true
"strictFunctionTypes": true
"strictPropertyInitialization": true
"noImplicitThis": true
  • strictNullChecks
    🍪 해당 에러를 해결하는 4가지 방법
    • (1) type guard

      1
      2
      3
      4
      5
      6
      7
      
      const div = document.querySelector('div');
      funciton clearDiv() {
          if(!div) {
               return;
          }
          div.innerHTML = '';
      }
      
    • type assertions
    ⚠️ 확신이 있을 때만 사용해야한다. 타입에러가 나지 않고, 객체의 경우 필요한 속성을 누락할 수 있다.
    (2) as
    (3) non-null type assertion: !
    (4) optional chaining operator: ? 권고
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
     // (1)
     const div = document.querySelector('div') as HTMLDivElement;
    
     // (2) !: null이 아니다
     div!.innerHTML ='';
    
     // (3)
     div?.innerHTML = '';
     // 내부적으로 해당 동작과 같다
     // if (div === null || div === undefined) {
     //   return;
     // } else {
     //   div.innerHTML = '';
     // }
    
    📕 타입스크립트 내부적으로 정의된 타입 간의 상하 관계
    HTMLDivElement extends HTMLElement extends Element
    MouseEvent extends UIEvent extends Event
  • strictFunctionTypes
    🐖 No overload matches this call.
    정의된 함수의 스펙과 넘겨받은 함수의 스펙이 일치하지 않을 때 해당 에러 발생

예제: DOM API 관련 유틸리티 함수 선언

⚔️ 예제: DOM API 관련 유틸리티 함수 선언
1
2
3
4
5
6
7
8
9
10
11
12
function getUnixTimestamp(date: string | Date) {
  return new Date(date).getTime();
}

funtion $<T extends HTMLElement = HTMLDiveElement>(selector: string) {
  const element = document.querySelector(selector);
  return element as T;
}
// Generic으로 타입까지 넘겨받으면 해당 함수 안에서 type assertion까지 할 수 있다.
// 넘겨받는 타입이 없을 경우를 대비하여, default타입을 위와 같이 정의할 수 있다.

const canvas = $<HTMLCanvasElement>('.canvasEle');

Utility Type

🍝 기존에 정의한 타입을 변환할 때 사용할 수 있는 타입 문법이다

🎯 유틸리티 타입을 이용하면 불필요하게 중복되는 타입 코드들을 줄여나갈 수 있다.

  • Partial
    특정 타입의 부분집합을 만족하는 타입을 정의할 수 있다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    interface Address {
      email: string;
      address: string;
    }
    
    type MayHaveEmail = Partial<Address>;
    const me: MayHaveEmail = {}; // (ㅇ)
    const you: MayHaveEmail = { email: "abc@abc.com" }; // (ㅇ)
    const all: MayHaveEmail = { email: "peter@abc.com", address: "Seoul" }; // (ㅇ)
    
  • Pick
    특정 타입에서 지정된 속성만 골라 타입을 정의
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    interface Product {
      id: number;
      name: string;
      price: number;
      brand: string;
      stock: number;
    }
    
    interface ProductOutline {
      id: number;
      name: string;
      price: number;
    }
    
    type ShoppingItem = Pick<Produce, "id" | "name" | "price">;
    
  • Omit
    특정 타입에서 지정된 속성만 제거한 타입을 정의
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    interface AddressBook {
      name: string;
      phone: number;
      address: string;
      company: string;
    }
    
    const chingtao: Omit<AddressBook, "address" | "company"> = {
      name: "중국집",
      phone: "1234123412"
    };
    

Mapped Type

🐙 Mapped Type

기존에 정의된 타입을 변환할 때 사용하는 문법인데,
js의 map API 함수를 타입에 적용한 것과 같은 효과를 가진다.

🎯 내부 구현체로 많이 활용된다.

🍪 Mapped Type 기본 문법

1
2
3
4
{ [ P in K ]: T }
{ [ P in K ]?: T }
{ readonly [ P in K ]: T}
{ readonly [ P in K ]?: T }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Person = "Peter" | "Jack" | "Lami";
type PersonAges = { [K in Person]: number };
// 기존에 정의된 타입을 Mapped Type 문법을 이용해 새로운 타입으로 변환(?)

// 아래와 동일한 타입 코드이다
// type PersonAges = {
//   Peter: number;
//   Jack: number;
//   Lami: number;
// }

const participantsAges: PersonAges = {
  Peter: 30,
  Jack: 45,
  Lami: 27
};

Utility Type 내부동작 구현

🌋 Mapped Type으로 구현된 Partial 유틸리티 함수는 다음과 같이 정의된다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
interface UserProfile {
  username: string;
  email: string;
  profilePhotoUrl: string;
}

type UserProfileUpdate = Particial<UserProfile>;
// 아래와 동일한 타입코드이다.
// type UserProfileUpdate = {
//   username?: string;
//   email?: string;
//   profilePhotoUrl?: string;
// }

// #1
type UserProfileUpdate = {
  username?: UserProfile["username"];
  email?: UserProfile["email"];
  profilePhotoUrl?: UserProfile["profilePhotoUrl"];
};

// #2, Mapped Type 문법 이용
type UserProfileUpdate = {
  [p in "username" | "email" | "profilePhotoUrl"]?: UserProfile[p];
};

// #3, keyof 문법 이용
type UserProfileUpdate = {
  [p in keyof UserProfile]?: UserProfile[p];
};

// #3, 기존에 정의된 다른 타입을 넘겨받을 수 있게, Generic 이용
type Subset<T> = {
  [p in keyof T]?: T[p];
};

// Partial은 내부적으로 이렇게 정의되어 있다.
This post is licensed under CC BY 4.0 by the author.