타입스크립트 용법과 구성(TypeScript Usage Or Config)

Minho Yoo·2022년 12월 14일
1

TypeScript

목록 보기
3/3
post-thumbnail

Usage

모듈

타입스크립트에서 가리키는 모듈이라는 개념은 ES6+의 Modules 개념과 유사하다.
모듈은 전역 변수와 구분되는 자체 유효 범위를 가지며 export, import와 같은 키워드를 사용하지 않으면 다른 파일에 접근할 수 없다.

Export

ES6의 export와 같은 방식으로 변수, 함수, 타입, 인터페이스 등에 붙여 사용한다.

// math.ts
export interface Triangle {
  width: number;
  height: number;
}

// index.ts
import { Triangle } from './math.ts';

class SomeTriangle implements Triangle {
  // ...
}

Import

ES6의 import와 동일한 방식으로 사용한다.

import { WheatBeerClass } from 'index.ts';

class Cloud extends WheatBeerClass {

}

타입스크립트는 모듈 코드를 어떻게 변환해주는가?

tsconfig.json 파일에 설정한 컴파일러 모드에 따라 모듈 코드가 각기 다르게 변환된다.

// SimpleModule.ts
import m = require('mod');
export let t = m.something + 1
// AMD / RequireJS SimpleModule.js
define(["require", "exports", "./mod"]), function (require, exports, mod_1) {
  exports.t = mod_1.something + 1;
}
// CommonJS / Node SimpleModule.js
var mod_1 = require("./mod");
exports.t = mod_1.something + 1;
// UMD SimpleModule.js
(function (factory) {
  if (typeof module === "object" && typeof module.exports === "object") {
    var v = factory(require, exports); if (v !== undefined) module.exports = v;
  }
  else if (typeof define === "function" && define.amd) {
    define(["require", "exports", "./mod"], factory);
  }
})(function (require, exports) {
  var mod_1 = require("./mod");
  exports.t = mod_1.something + 1;
});
// System SimpleModule.js
System.register(["./mod"], function(exports_1) {
  var mod_1;
  var t;
  return {
    setters: [
      function (mod_1_1) {
        mod_1 = mod_1_1;
      }],
    execute: function() {
      exports_1("t", t = mod_1.something + 1);
    }
  }
})

타입스크립트 컴파일 명령어를 칠 때 컴파일러 모드를 부여할 수 있다.

# commonjs 모드인 경우
tsc --module commonjs Test.ts

# amd 모드인 경우
tsc --module amd Test.ts

선택적 모듈 로딩 방법과 고급 모듈 로딩 기법

TBD

d.ts 파일

타입스크립트 선언 파일

타입스크립트 선언 파일 d.ts는 타입스크립트 코드의 타입 추론을 돕는 파일이다.
예를 들어, 전역 변수로 선언한 변수를 특정 파일에서 import 구문 없이 사용하는 경우 해당 변수를 인식하지 못한다.
그럴 때 아래와 같이 해당 변수를 선언해서 에러가 나지 않게 할 수 있다.

declare const global = 'sth';

전역 변수와 전역 함수에 대한 타입 선언

해당 타입스크립트 파일에서 사용할 순 있지만 선언되어 있지 않은 전역 변수나 전역 함수는 아래와 같이 타입을 선언할 수 있다.

// 전역 변수
declare const pi = 3.14;

// 전역 함수
declare namespace myLib {
  function greet(person: string): string;
  let name: string;
}
myLib.greet('캡틴');
myLib.name = '타노스';

TIP

Use declare namespace to describe types or values accessed by dotted notation.

참고 자료

https://basarat.gitbook.io/typescript/main/common-errors

인덱싱

타입스크립트에서 배열과 객체를 인덱싱 하는 방법

타입스크립트에서 배열 요소와 객체의 속성을 접근할 때는 인터페이스를 사용하면 된다.

배열 요소 접근

자바스크립트에서는 아래와 같이 배열의 요소를 접근했다.

const arr = ['Thor', 'Hulk'];
arr[0]; // 'Thor'

타입스크립트에서는 인터페이스를 이용하여 아래와 같이 인덱싱 타입을 정의할 수 있다.

interface StringArray {
  [index: number]: string;
}

const arr: StringArray = ['Thor', 'Hulk'];
arr[0]; // 'Thor'

배열의 인덱스의 타입은 숫자이고 이 인덱스로 특정 요소를 접근했을 때 해당 요소의 타입은 string 이라는 것을 명시했다.

타입으로 배열 변경 제한하기

배열에 강한 타입을 적용함과 동시에 배열의 요소롤 변경하지 못하게 하려면 아래와 같이 한다.

interface ReadonlyStringArray {
  readonly [index: number]: string;
}

const arr: ReadonlyStringArray = ['Thor', 'Hulk'];
arr[2] = 'Capt'; // Error!

유틸리티 타입

유틸리티 타입은 이미 정의해 놓은 타입을 변환할 때 사용하기 좋은 타입 문법이다.
유틸리티 타입은 꼭 쓰지 않더라도 기존의 인터페이스, 제네릭 등의 기본 문법으로 충분히 타입을 변환할 수 있지만 유틸리티 타입을 쓰면 훨씬 더 간결한 문법으로 타입을 정의할 수 있다.

자주 사용되는 유틸리티 타입 몇 개 알아보기

자주 사용되는 타입을 몇 개 알아보겠다.

Partial

파셜(Partial) 타입은 특정 타입의 부분 집합을 만족하는 타입을 정의할 수 있다.

interface Address {
  email: string;
  address: string;
}

type MayHaveEmail = Partial<Address>;
const me: MayHaveEmail = {}; // 가능
const you: MayHaveEmail = { email: 'test@abc.com' }; // 가능
const all: MayHaveEmail = { email: 'capt@hero.com', address: 'Pangyo' }; // 가능

Pick

픽(Pick) 타입은 특정 타입에서 몇 개의 속성을 선택(pick)하여 타입을 정의할 수 있다.

interface Hero {
  name: string;
  skill: string;
}

const human: Pick<Hero, 'name'> = {
  name: '스킬이 없는 사람',
}
type HasThen<T> = Pick<Promise<T>, 'then' | 'catch'>;
let hasThen: HasThen<number> = Promise.resolve(4);
hasThen.th // 위에서 'then'만 선택하면 'then'만 제공, 'catch' 선택하면 'catch만 제공'

Omit

오밋(Omit) 타입은 특정 타입에서 지정된 속성만 제거한 타입을 정의해 준다.

interface AddressBook {
  name: string;
  phone: number;
  address: string;
  company: string;
}
const phoneBook: Omit<AddressBook, 'address'> = {
  name: '재택근무',
  phone: 123422223333,
  company: '내 방'
}
const chingtao: Omit<AddressBook, 'address'|'company'> = {
  name: '중국집',
  phone: 44455557777
}

맵드 타입

맵드 타입이란 기존에 정의되어 있는 타입을 새로운 타입으로 변환해 주는 문법을 의미한다.
마치 자바스크립트 map() API 함수를 타입에 적용한 것과 같은 효과를 가진다.

자바스크립트 map 함수란?

자바스크립트 map AP는 배열을 다룰 때 유용한 자바스크립트 내장 API이다.

var arr = [{ id: 1, title: '함수' }, { id: 2, title: '변수' }, { id: 3, title: '인자' }];
var result = arr.map(function(item) {
  return item.title;
});
console.log(result); // ['함수', '변수', '인자'];

위 코드는 3개의 객체를 요소로 가진 배열 arr.map() API를 적용한 코드이다.
배열의 각 요소를 순회하여 객체(id, title)에서 문자열로 변환하였다.

맵드 타입의 기본 문법

맵드 타입은 위에서 살펴본 자바스크립트 map 함수를 타입에 적용했다고 보면 된다.
이를 위해서는 아래와 같은 형태의 문법을 사용해야 한다.

{ [P in K]: T }
{ [P in K]?: T }
{ readonly [P in K]: T }
{ readonly [P in K]?: T }

맵드 타입 기본 예제

맵드 타입의 가장 간단한 예제를 보겠다.
아래와 같이 헐크, 토르, 캡틴을 유니온 타입으로 묶어주는 Heroes라는 타입이 있다고 하겠다.

type Heroes = 'Hulk' | 'Thor' | 'Capt';

여기서 이 세 영웅의 이름이 각각 나이까지 붙인 객체를 만들고 싶다고 한다면 아래와 같이 변환할 수 있다.

type HeroProfiles = { [K in Heroes]: number };
const heroInfo: HeroProfiles = {
  Hulk: 54,
  Thor: 1000,
  Capt: 33
}

위 코드에서 [K in Heroes] 부분은 마치 자바스크립트의 for in 문법과 유사하게 동작한다.
앞으로 정의한 Heroes 타입의 3개의 문자열을 각각 순회하여 number 타입을 값으로 가지는 객체의 키로 정의가 된다.
예를 들면 아래와 같이 정의가 된다.

{ Hulk: number } // 첫번째 순회
{ Thor: number } // 두번째 순회
{ Capt: number } // 세번째 순회

따라서 위의 원리가 적용된 HeroProfiles의 타입은 아래와 같이 정의된다.

type HeroProfiles = {
  Hulk: number;
  Thor: number;
  Capt: number;
}

맵드 타입 실용 예제1

앞에서 살펴본 예제는 맵드 타입의 문법과 동작을 위해하기 위해 간단한 코드를 사용했다.
실제로 서비스를 개발할 때는 위와 같은 코드보다는 아래와 같은 코드를 더 많이 사용하게 된다.

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

위 코드는 키와 값이 있는 객체를 정의하는 타입을 받아 그 객체의 부분 집합을 만족하는 타입으로 변환해 주는 문법이다.
예를 들면 아래와 같은 인터페이스가 있다고 할 때

interface Person {
  age: number;
  name: string;
}

Subset 타입을 적용하면 아래와 같은 객체를 모두 정의할 수 있다.

const ageOnly: Subset<Person> = { age: 23 };
const nameOnly: Subset<Person> = { name: 'Tony' };
const ironman: Subset<Person> = { age: 23, name: 'Tony' };
const empty: Subset<Person> = {};

맵드 타입 실용 예제2

아래와 같이 사용자 프로필을 조회하는 API 함수가 있다고 했을 때

interface UserProfile {
  username: string;
  email: string;
  profilePhotoUrl: string;
}

function fetchUserProfile(): UserProfile {
  // ...
}

이 프로필의 정보를 수정하는 API는 아마 아래와 같은 형태일 것이다.

interface UserProfileUpdate {
  username?: string;
  email?: string;
  profilePhotoUrl?: string;
}

function updateUserProfile(params: UserProfileUpdate) {
  // ...
}

이 때 아래와 같이 동일한 타입에 대해서 반복해서 선언하는 것을 피해야 한다.

interface UserProfile {
  username: string;
  email: string;
  profilePhotoUrl: string;
}

interface UserProfileUpdate {
  username?: string;
  email?: string;
  profilePhotoUrl?: string;
}

위의 인터페이스에서 반복되는 구조를 아래와 같은 방식으로 재활용 할 수 있다.

type UserProfileUpdate = {
  username?: UserProfile['username'];
  email?: UserProfile['email'];
  profilePhotoUrl?: UserProfile['profileUPhotoUrl'];
}

혹은 좀 더 줄여서 아래와 같이 정의할 수도 있다.

type UserProfileUpdate = {
  [p in 'username' | 'email' | 'profilePhotoUrl']?: UserProfile[p]
}

여기서 위 코드에 keyof를 적용하면 아래와 같이 줄일 수 있다.

type UserProfileUpdate = {
  [p in keyof UserProfile]?: UserProfile[p]
}

Config

tsconfig

타입스크립트 설정 파일(tsconfig.json)

타입스크립트 설정 파일은 타입스크립트를 자바스크립트로 변환할 때의 설정을 정의해놓는 파일이다.
프로젝트에서 tsc라는 명령어를 치면 타입스크립트 설정 파일에 정의된 내용을 기준으로 변환 작업(컴파일)을 진행한다.

tsc 명령어

tsc 명령어는 타입스크립트를 자바스크립트로 변환할 때 사용하는 명령어이다.
아래와 같이 입력하면 app.ts 파일이 app.js 로 변환된다.

tsc app.ts

타입스크립트 설정 파일 인식 기준

tsc 명령어를 대상 파일을 지정하지 않고 실행하면 현재 폴더에 있는 타입스크립트 설정 파일을 기준으로 변환 작업을 수행한다.
만약 현재 폴더에 타입스크립트 설정 파일이 없다면 프로젝트 폴더 내에서 상위 폴더의 경로를 검색해 나간다.

tsconfig.json 파일의 경로를 명시적으로 지정하고 싶다면 아래 명령어를 사용하면 된다.

# 형식 예시
tsc --project 상대 경로

# 실제 명령어 예시
tsc --project ./src
tsc --p ./src

타입스크립트 설정 파일 속성

files

타입스크립트 변환 명령어를 입력할 때마다 대상 파일을 경로를 지정하지 않고 설정 파일을 미리 정의해 놓을 수 있다.

{
  "files": ["app.ts", "./utils/math.ts"]
}

include

files와 같이 파일을 개별로 지정하지 않고 include 옵션으로 변환할 폴더를 지정할 수 있다.

{
  "include": ["src/**/*"]
}

TIP

와일드 카드 패턴
*: 해당 디렉토리의 모든 파일 검색
?: 해당 디렉토리 안에 파일의 이름 중 한글자라도 맞으면 해당
**: 해당 디렉토리를 재귀적으로 접근(하위 디렉토리의 하위 티렉토리가 존재하는 경우 반복해서 접근)

WARNING

⚠ 위 와일드 카드 패턴에 해당하는 파일 확장자는 js, jsx, ts, tsx, d.ts 이다.

exclude

include 와 반대되는 속성으로 변환하지 않을 폴더 경로를 지정한다.

{
  "exclude": ["node_modules"]
}

만약 설정하지 않으면 기본적으로 node_modules, bower_componenets 같은 폴더를 제외한다.

TIP

컴파일 대상 경로를 정의하는 속성의 우선 순위 files > include = exclude

@types 라이브러리와 typeRoots

타입스크립트 설정 파일은 기본적으로 node_modules 를 제외하면 써드 파티 라이브러리의 타입을 정의해놓는 @types 폴더는 컴파일에 포함된다.
예를 들면 아래와 같다.

└─ node_modules
   ├─ @types => 컴파일에 포함
   ├─ lodash => 컴파일에서 제외

그런데 여기서 만약 @types의 기본 경로를 변경하고 싶다면 아래와 같이 지정할 수 있다.

{
  "compilerOptions": {
    "typeRoots": ["./my-types"]
  }
}

이렇게 되면 타이핑 라이브러리(써드 파티 라이브러리의 타입 정의 라이브러리)의 대상 경로가 node_modules/@types에서 node_modules/my-types로 변경된다.

extends

특정 타입스크립트 설정 파일에서 다른 타입스크립트 설정의 내용을 가져와 추가할 수 있는 속성이다.

// config/base.json
{
  "compilerOptions": {
    "noImplicitAny":" true
  }
}
// tsconfig.json
{
  "extends": "./config/base"
}

확장난 파일의 내용을 가져다가 덮어쓰거나 새로 정의할 수도 있다.

target

타입스크립트 파일을 컴파일 했을 때 빌드 디렉토리에 생성되는 자바스크립트의 버전을 의미한다.
기본값인 es3 부터 es5, es6 등 가장 최신 버전인 esnext까지 있다.

// tsconfig.json
{
  "target": "esnext"
}

lib

타입스크립트 파일을 자바스크립트로 컴파일 할 때 포함될 라이브러리의 목록이다.
이 속성을 활용하는 가장 대표적인 사례는 async 코드를 컴파일 할 때 Promise 객체가 필요하므로 아래와 같은 설정을 해줘야 한다.

// tsconfig.json
{
  "lib": ["es2015", "dom", "dom.iterable"]
}

여기서 es2015는 프로미스 객체를 타입스크립트에서 인식할 수 있게 필요한 속성이고, dom 관련 속성은 DOM API를 사용하는 경우 필요하다.

allowJs

타입스크립트 컴파일 작업을 진행할 때 자바스크립트 파일도 포함할 수 있는지를 설정해주는 속성이다.
주로 이미 기존에 존재하는 자바스크립트 프로젝트에 타입스크립트를 점진적으로 적용할 때 사용하면 좋은 속성이다.

types 라이브러리

자바스크립트로 만들어진 써드 파티 라이브러리(jQuery, lodash, chart 등)를 타입스크립트에서 사용하려면 각 기능에 대한 타입이 정의되어 있어야 한다.
예를 들면 아래와 같은 코드는 타입스크립트에서 제대로 동작하지 않는다.

// app.ts
import $ from 'jquery';

$(document).ready();

그 이유는 제이쿼리 라이브러리의 내부 코드에 대한 타입이 정의되어 있지 않아 이 라이브러리를 들고 와서 사용할 때 타입스크립트 파일에서 타입 추론을 할 수 없기 때문이다.
이런 경우에는 @types 라는 라이브러리를 설치하면 된다.

npm i -D @types/jquery

대중적으로 흔히 사용되는 자바스크립트 라이브러리는 대부분 @types 라는 별칭으로 타입스크립트 추론이 가능한 보조 라이브러리를 제공한다.
만약 이 라이브러리가 없는 경우에는 [스스로 선언하거나 다른 방법](아래 내용 작성 후 링크 제공)을 찾아봐야 한다.

@types 라이브러리 내부 구조

types 라이브러리는 일반적으로 index.d.ts 파일과 package.json 파일로 구성되어 있다.

└─ @types/jquery
   ├─ index.d.ts
   ├─ package.json

package.json 파일 안에는 types 속성이 정의되어 있다.

Keep in mind that automatic inclusion is only important if you’re using files with global declarations (as opposed to files declared as modules).

types 라이브러리가 없는 경우

TBD

참고 자료

JS에 TS 적용하기

자바스크립트 코드에 타입스크립트를 적용할 때 주의해야 할 점

  • 기능적인 변경은 절대 하지 않을 것
  • 테스트 커버리지가 낮을 땐 함부로 타입스크립트를 적용하지 않을 것
  • 처음부터 타입을 엄격하게 적용하지 않을 것 (점진적으로 strict 레벨을 증가)

자바스크립트 프로젝트에 타입스크립트 적용하는 절차

자바스크립트 프로젝트에 타입스크립트를 적용하는 경우 아래의 절차를 따르면 도움이 된다.

TIP

TLDR
1. 타입스크립트 환경 설정 및 ts 파일로 변환
2. any 타입 선언
3. any 타입을 더 적절한 타입으로 변경

1. 타입스크립트 프로젝트 환경 구성

  • 프로젝트 생성 후 NPM 초기화 명령어로 package.json 파일을 생성한다.
  • 프로젝트 폴더에는 npm i typescript -D로 타입스크립트 라이브러리를 설치한다.
  • 타입스크립트 설정 파일 tsconfig.json을 생성하고 기본 값을 추가한다.
{
  "compilerOptions": {
    "allowJs": true,
    "target": "ES5",
    "outDir": "./dist",
    "moduleResolution": "Node",
    "lib": ["ES2015", "DOM", "DOM.Iterable"]
  },
  "include": ["./src/**/*"],
  "exclude": ["node_modules", "dist"]
}
  • 서비스 코드가 포함된 자바스크립트 파일을 타입스크립트 파일로 변환한다.
  • 타입스크립트 컴파일 명령어 tsc로 타입스크립트 파일을 자바스크립트 파일로 변환한다.

2. 엄격하지 않은 타입 환경(loose type)에서 프로젝트 돌려보기

  • 프로젝트에 테스트 코드가 있다면 테스트 코드가 통과하는지 먼저 확인한다.
  • 프로젝트의 js 파일을 모두 ts 파일로 변경한다.
  • 타입스크립트 컴파일 에러가 나는 것 위주로만 먼저 에러가 나지 않게 수정한다.
    • 여기서, 기능을 사소하게라도 변경하지 않도록 주의한다.
  • 테스트 코드가 성공하는지 확인한다.

3. 명시적인 any 선언하기

  • 프로젝트 테스트 코드가 통과하는지 확인한다.
  • 타입스크립트 설정 파일에 noImplicitAny: true를 추가한다.
  • 가능한 타입을 적용할 수 있는 몯느 곳에 타입을 적용한다.
    • 라이브러리를 쓰는 경우 DefinitelyTyped에서 @types 관련 라이브러리를 찾아 설치한다.
    • 만약, 타입을 정하기 어려운 곳이 있으면 명시적으로라도 any를 선언한다.
  • 테스트 코드가 통과하는지 확인한다.

4. strict 모드 설정하기

  • 타입스크립트 설정 파일에 아래 설정을 추가한다.
{
  "strict": true,
  "strictNullChecks": true,
  "strictFunctionTypes": true,
  "strictBindCallApply": true,
  "strictPropertyInitialization": true,
  "noImplicitThis": true,
  "alwaysStrict": true
}
  • any로 되어 있는 타입을 최대한 적절한 타입으로 변환한다.
  • as와 같은 키워드를 최대한 사용하지 않도록 고민해서 변경한다.
profile
Always happy coding 😊

0개의 댓글