TypeScript

web3means
TypeScript

Studying TypeScript and Memo

commands

initialize

tsc init

tsconfig.json 생성

watch mode

tsc ${filename} -w

원본 .ts 파일이 수정될 때 마다, .js 파일로 컴파일

tsconfig.json

tsconfig.jsonproperty에 관해 정리

outDir

compilerOptions, 컴파일 완료 된 .js 파일을 출력할 경로

rootDir

compilerOptions, 컴파일을 실행할 위치, 지정한 위치 상위에 .ts 파일이 있으면 에러 반환

incremental

compilerOptions, 이전 컴파일과 비교해서 수정된 사항에 안해서만 컴파일 실행

target

compilerOptions, 어떤 버전으로 컴파일 할 것인지 설정. 하위호환을 위해 상황에 관계 없이 낮은 버전을 사용하는 것은 지양. ES5 혹은 ES6를 많이 씀

module

compilerOptions, Node.js 프로젝트의 경우 CommonJS (import가 아닌 require 이용), 브라우저 환경일 경우 ES 표준에 맞게 설정

lib

compilerOptions, 세부적으로 어떤 library를 이용할 것인지 설정. 보통은 따로 설정하지 않고 target 설정시 사용되는 기본 lib를 사용

allowJs

compilerOptions, 프로젝트 안에서 .js를 같이 사용할 것인지 여부 설정

checkJs

compilerOptions, .js 파일에서 에러가 있다면 경고 표시

jsx

compilerOptions, react에서 사용되는 .jsx 파일에 관한 설정

declaration

compilerOptions, type 정의 관련. 작성한 코드가 라이브러리로서 사용되는 경우에 주로 사용. 일반 프로젝트에서는 사용하지 않음

sourceMap

compilerOptions, 디버깅을 위해서 사용

outFile

compilerOptions, 작성한 여러 .ts를 하나의 .js로 컴파일할 때 사용

composite

compilerOptions, incremental과 같이 사용. 빌드 정보를 저장하여 이후에 있을 빌드를 빠르게 할 수 있도록 함

tsBuildInfoFIle

compilerOptions, incremental의 설정이 true일 경우 그에 관련된 정보를 저장하는 파일을 출력하도록 설정

removeComments

compilerOptions, 주석을 제거할것인지의 설정

noEmit

compilerOptions, 컴파일 에러 체크만 하고, .js 파일을 반환하지는 않음. 컴파일 에러가 있는지 없는지 확인만 하고 싶을 때 사용

isolatedModules

compilerOptions, 각각의 파일을 다른 module로 변환해서 컴파일 실행하도록 하는 설정

exclude

컴파일을 제외할 파일

include

컴파일을 할 파일. 이외의 것들은 컴파일 되지 않음

from lecture 1

Implicit Types vs Explicit Types

tstype inference (타입 추론)을 통해 type을 명시적으로 표기하지 않는 경우에도 묵시적으로 정의해준다.

let a = "hello"; // Implicit
let b: boolean = false; // explicit

Types of TS

Object

const player: {
  name: string;
  age?: number;
} = {
  name: "nico",
};

Alias Type

type Age = number;
type Name = string;
type Player = {
  name: Name;
  age?: Age;
};

const player: Player = {
  name: "nico",
};

type on function

function playerMaker(name: string): Player {
  return {
    name,
  };
}

화살표 함수의 경우, 아래와 같이 작성

const playerMaker = (name: string): Player => ({
  name,
});

read-only

type Player = {
  readonly name: Name;
  age?: Age;
};

const numbers: readonly number[] = [1, 2, 3, 4];
numbers.push(5); // error occur here

Tuple

  • TupleArray를 생성할 수 있게 함
  • 최소한의 길이를 가져야 함
  • 특정 위치에 특정 타입이 있어야 함
const player: [string, number, boolean] = ["nico", 4, true];
const playerReadOnly: readonly [string, number, boolean] = ["nico", 4, true]; // cf. readonly also available on Tuple

any

값이 비어있다면 기본 타입값이 any

let a = []; // 이 경우, type은 any[] (array of any) 임

unknwon

type을 알 수 없을 때 사용

다른 API 등으로 부터 데이터를 받아 왔을 때, 해당 데이터의 type을 모를 경우 등에 사용

이 때, if 문 등으로 type을 먼저 체크한 후, 해당 type에서 가능한 연산을 해주는 것은 가능

let a: unknown;
if (typeof a === "number") {
  let b = a + 1;
}
if (typeof a === "string") {
  let b = a.toUpperCase();
}

void

void는 아무것도 return하지 않는 함수에서의 반환 type

보통 void를 명시적으로 작성해줄 필요는 없음

function hello() {
  console.log("something");
} // return void

// same as
// function hello() : void {
//  console.log("somnething);
// }

never

함수가 절대 return 하지 않을 경우에 사용

함수에서 exception (예외) 등이 발생할 때 사용

또, type이 두가지 일 수 도 있는 경우에도 사용

function hello(): never {
  throw new Error("something wrong");
  // return 이 아닌 error throw 발생
}
function hello(name: string | number): never {
  if (typeof name === "string") {
    name; // name의 type이 string
  } else if (typeof name === "number") {
    name; // name의 type이 number
  } else {
    name; // name의 type이 never
    // 절대 실행되지 않는 부분
  }
}

functions

typescript에서의 function 에 대해 다룬 내용

Arrrow function

const add = (a: number, b: number) => a + b;

Call Signatures

type Add = (a: number, b: number) => number; // Call Signature

const add: Add = (a, b) => a + b;

또는, 다음과 같은 방법으로 작성

type Add = {
  (a: number, b: number): number;
};

const add: Add = (a, b) => a + b;

Overloading

Overloading은 함수가 여러 개의 Call Signature 를 가지고 있을 때 발생

type Add = {
  (a: number, b: number): number;
  (a: number, b: string): number;
};

const add: Add = (a, b) => {
  if (typeof b === "string") return a;
  return a + b;
};

다음은 next.js 에서의 Router 에서의 Push 메소드에서의 Overloading 에시임

type Config = {
  path: string;
  state: object;
};

type Push = {
  (path: string): void;
  (obj: Config): void;
};

const push: Push = (config) => {
  if (typeof config === "string") console.log(config);
  else {
    console.log(config.path);
  }
};

또 아래와 같이 parameter 개수에 따라서도 Overloading 을 사용함

type Add = {
  (a: number, b: number): number;
  (a: number, b: number, c: number): number;
};

const add: Add = (a, b, c?: number) => {
  if (c) return a + b + c;
  return a + b;
};

Polymorphism

여러 다른 구조, 여러 다른 형태를 뜻함

type SuperPrint = {
  (arr: number[]): void;
  (arr: boolean[]): void;
  (arr: string[]): void;
};

const superPrint: SuperPrint = (arr) => {
  arr.forEach((i) => console.log(i));
};

superPrint([1, 2, 3, 4]);
superPrint([true, false, true]);
superPrint(["a", "b", "c"]);
superPrint([1, 2, true, false]); // Call Signature 가 존재하지 않기 때문에 동작하지 않음

위의 코드 예시와 같이 형태를 일일히 Call Signature 로 표현하기 보다, 아래와 같이 generic 을 사용할 수 있음

type SuperPrint = {
  <T>(arr: T[]): void;
};

const superPrint: SuperPrint = (arr) => {
  arr.forEach((i) => console.log(i));
};

superPrint([1, 2, 3, 4]);
superPrint([true, false, true]);
superPrint(["a", "b", "c"]);
superPrint([1, 2, true, false]); // 모두 동작

즉, generic 을 통해 Polymorphism 을 구현한 것?

Generics Recap

아래는 generic을 활용한 에제 코드이다

type Player<E> = {
  name: string;
  extraInfo: E;
};
type NicoExtra = {
  favFood: string;
};
type NicoPlayer = Player<NicoExtra>;

const nico: NicoPlayer = {
  name: "nico",
  extraInfo: {
    favFood: "kimchi",
  },
};

const lynn: Player<null> = {
  name: "lynn",
  extraInfo: null,
};

Classes and Interfaces

typescript에서의 ClassInterface 에 대해 다룬 내용

Class

다음과 같이 class 를 선언하고 이용한다

class Player {
  constructor(
    private firstName: string,
    private lastName: string,
    public nickname: string
  ) {}
}

const nico = new Player("nico", "las", "nc");

아래와 같이 abstract class 또한 지원한다

abstract class 는 다른 class 가 상속받을 수 있는 class 를 의미한다

단, abstract class 로는 instance 를 생성하지 못한다

말그대로, 추상적으로 존재하는 class

abstract class User {
  constructor(
    private firstName: string,
    private lastName: string,
    public nickname: string
  ) {}
}

class Player extends User {}

아래의 코드는 class 내에서 method 를 정의하는 예제 코드

abstract class User {
  constructor(
    private firstName: string,
    private lastName: string,
    public nickname: string
  ) {}
  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

또, abstract class 내에서는 abstract method 도 정의할 수 있다

이 때, method 의 동작을 구현해서는 안되고, methodCall Signature 만 정의해야한다

abstract methodabstract class 를 상속받는 모든 class 들이 구현을 해야하는 method 를 의미한다

abstract class User {
  constructor(
    private firstName: string,
    private lastName: string,
    protected nickname: string
  ) {}
  // abastact getNickName(arg : string): void
  abastact getNickName(): void
  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

class Player extends User {
  getNickName (){ // 추상 메소드 이므로, 상속 받은 클래스 내에서 구현해야한다
    return this.nickname;
  }
}

아래 부터는 class 를 활용하여 HashMap (dictionary 와 유사) 을 구현하는 에제이다

type Words = {
  [key: string]: string; // property의 이름은 모르고, type 을 알 때, [keyName : type] 과 같은 형식으로 정의
};

class Dict {
  private words: Words;
  constructor() {
    this.words = {};
  }
  add(word: Word) {
    // class 또한 type 처럼 사용 가능
    if (this.words[word.term] === undefined) {
      this.words[word.term] = word.def;
    }
  }
  def(term: string) {
    return this.words[term];
  }
}

class Word {
  constructor(public readonly term: string, public readonly def: string) {}
}

const kimchi = new Word("kimchi", "korean food");

const dict = new Dict();

dict.add(kimchi);
dict.def("kimchi");

Interface

type에 비해 Interface 는 특히 오브젝트의 형태를 특정해주기 위한 용도로만 사용

Interface 또한 type 으로 사용할 수 있음

오브젝트의 형태를 특정하는 법에는

type 또는 Interface 두 가지 방법이 있으나, type의 경우에는 주로 타입 표기를 위해 사용함

Interfaceextends 를 통해 쉽게 상속이 가능하지만, type의 경우에는 상속을 & 연산자로 표기해야함

type team = "red" | "yellow" | "blue";
type health = 0 | 5 | 10;
type Player = {
  nickname: string;
  team: Team;
  health: Health;
};

interface Player {
  nickname: string;
  team: Team;
  health: Health;
}

const nico: Player = {
  nickname: "nico",
  team: "yellow",
  health: 10,
};
interface User {
  name: string;
}

interface Player extends User {}

const nico: Player = {
  name: "nico",
};

type User = {
  name: string;
};
type Player = User & {};

const nico: Player = {
  name: "nico",
};

또, interface 를 통해 abstract class의 구현을 대체할 수 있다

이 때 해당 interface를 이용하는 classimplements 키워드로 구현한다

이 경우, abstract class 를 사용하는 것보다 컴파일 결과물인 .js 코드가 상대적으로 가벼워진다

implementsjs 에서는 지원하지 않기 때문에, 간결한 코드로 컴파일 되기 때문이다.

interface User {
  firstName: string;
  lastName: string;
  sayHi(name: string): string;
  fullName(): string;
}

class Player implements User {
  constructor(public firstName: string, public lastName: string) {}
  fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
  sayHi(name: string) {
    return `Hello ${name}. My name is ${this.fullName()}`;
  }
}

type vs interface

  • type 을 사용하고 싶다면, 말그대로 type 키워드를 사용
  • type 을 재활용 및 연장하여 상속하고 싶다면, & 키워드를 사용
  • interface 를 재활용 및 연장하여 상속하고 싶다면, extends 키워드를 사용
  • interface 의 명칭이 같다면, 해당 이름을 가진 interface는 여러 개의 속성이 자동으로 통합 됨

Polymorphism

  • Polymorphism (다형성) 은 다른 형태 및 모양의 코드를 가질 수 있도록 해줌
  • Polymorphism 을 이루기 위해서, generic 을 사용함
  • genericplaceholder 타입을 사용할 수 있도록 해줌
  • placeholder 타입이란, concrete 타입과는 상반되는 개념
  • compile이 실행되면, 컴파일러가 placeholder 타입을 concrete 타입으로 바꾸어줌

이하의 코드에서는 Polymorphism, generic, class, interface 등을 활용하여 브라우저에서 이용되는 localstorage 를 구현해보도록 함

interface SStorage<T> {
  [key: string]: T;
}

class LocalStorage<T> {
  private storage: SStorage<T> = {};
  set(key: string, value: T) {
    this.storage[key] = value;
  }
  remove(key: string) {
    delete this.storage[key];
  }
  get(key: string): T {
    return this.storage[key];
  }
  clear() {
    this.storage = {};
  }
}

const stringsStorage = new LocalStorage<string>();
stringStorage.get("key");

const booleanStorage = new LocalStorage<boolean>();
booleanStorage.get("xxx");

TS Settings

Nest.js, Next.js, Create-React-App 등 을 사용하는 대부분의 경우에는 수동으로 typescript 프로젝트를 설정할 기회가 거의 없다

이에, ts 설정법을 따로 이해해두는 편이 알고 쓰고자하는 방향성에는 부합

(webpack 또한 직접 설정할 일이 잘 없는 것과 유사)

Install TS

npm i -D typescript

-D 옵션을 통해, tsdevDependencies 에 설치되도록 함

Set tsconfig.json

프로젝트 최상위 경로에 tsconfig.json 파일을 생성한다

touch tsconfig.json

이후 tsconfig.json 파일 내에 아래와 같이 작성

{
  "include": ["src"],
  "compilerOptions": { "outDir": "build" }
}

include 옵션은, compile 대상이 되는 파일이 되는 경로를 작성

compilerOptions 옵션에서 outDir 속성은, 컴파일의 결과물인 .js 파일의 출력 경로를 작성

이후 package.json 파일에서, scripts 에 다음과 같이 작성

{
  "scripts": {
    "build": "tsc"
  }
}

위의 작업 후, terminal 에서 npm run build 혹은 tsc 명령어를 입력하면 컴파일의 결과물이 설정한 경로에 생성된다

{
  "scripts": {
    "build": "tsc",
    "start": "node build/index.js"
  }
}

이와 함께 위와 같이 start를 정의하면, 실행하도록 설정해줄 수도 있다

결국 npm run build && npm run start 와 같이 동작해야 하는데,

이를 용이하게 하기 위해 ts-node를 설치한다

npm i -D ts-node

ts-node는 빌드 없이 .ts 를 실행할 수 있게 해주는 프로덕션용이 아닌 개발 환경에서만 사용하는 패키지이다

설치 후, 아래와 같이 dev 를 정의해준다

{
  "scripts": {
    "build": "tsc",
    "start": "node build/index.js",
    "dev": "ts-node src/index"
  }
}

이후, 코드가 수정될 때마다 실행되게 설정하고 싶다면, nodemon 등을 활용한다

다음으로, comilerOptions 에서 outDir 외의 여러 속성을 설정 하여 컴파일 옵션을 바꿔줄 수 있다

가령, 아래와 같이 target 필드를 사용하면 해당 JavaScript 표준으로 컴파일 해준다

{
  "include": ["src"],
  "compilerOptions": { "outDir": "build", "target": "ES6" }
}

일반적으로 ES5 또는 ES6 를 이용하게 되는데, 이는 대부분의 node.js 버전과 대부분의 browser 가 현재 지원하고 있기 때문이다

다음으로 알아볼 lib 속성은, 합쳐진 라이브러리의 declaration file (정의 파일)을 특정해주는 역할을 함

즉, lib 속성은 JavaScript의 어떤 버전이 그 환경에서 사용되는지를 말함

작성할 코드가 ES6 를 지원하는 환경에서 실행될 것이라고 지정하고, 또 코드가 Browser 에서 동작할 것이라면 아래와 같이 작성

{
  "include": ["src"],
  "compilerOptions": { "outDir": "build", "target": "ES6" },
  "lib": ["ES6", "DOM"]
}

가령, lib 속성에 DOM 을 추가해주면, TypeScript 파일을 작성할 때, document. 을 입력했을 때 document 와 관련된 여러 메소드 및 인터페이스를 vs code에서 자동 제안함

예를 들어, document.qu 까지 작성한 경우, document.querySelector 를 자동 완성해주는 등, ts 자체적으로 라이브러리를 미리 구현해두었고,

이를 지원해준다는 측면에서 해당 필드명이 lib

Declaration Files

앞서 tsconfig.json 에서 언급한 declaration file (정의 파일)built-in JS APIs 라고 칭할 수 있다

즉, JavaScript 에서의 Math, Document, Map 등을 정의해둔 파일을 말한다

이러한 declaration file (정의 파일).d.ts 확장자로 정의된다

가령, myPacakge.jsnpm 에서 공유되는 module 이라 가정하고 아래와 같은 코드로 정의되어있다고 하자

export function init(config) {
  return true;
}

export function exit(code) {
  return code + 1;
}

두 개의 init, exit 함수로 구현되어있고, 이를 아래와 같이 .ts 파일에서 불러오면,

import { init, exit } from "myPackage";

init 함수에 대한 Call Signature 등이 정의 되어 있지 않기 때문에 오류가 발생한다

이에, 아래와 같은 코드로 myPackage.d.ts 의 이름으로 declaration file 을 작성해야 한다

interface Config {
  url: string;
}
declare module "myPackage" {
  function init(config: Config): boolean;
  function exit(code: number): number;
}

declaration file 을 작성한 후에는 더 이상 .ts 파일에서 오류를 발생시키지 않는다

실제로 .d.tsdeclaration file 을 작성할 일은 없는 편이지만, 마찬가지로 이해를 하고 사용하는 것이 중요한데,

JavaScript 로 작성된 많은 라이브러리 및 모듈들이 이와 같은 방식으로 TypeScript 에서 구동되기 때문이다

Using JavaScript File on TypeScript File

JavaScript 파일을 TypeScript 파일에 import 해서 사용하는 법에 대해 알아본다

이 경우, TypeScript 파일에서 아래와 같이 import 시에 경로명에 ./ 을 붙여준다

import { init, exit } from "./myPackage";

이후 tsconfig.json 에서 compilerOptions 이하에 아래와 같은 속성을 추가한다

{
  "compilerOptions": {
    "allowJs": true
  }
}

이 경우, any 타입 등으로 TypeScriptimportJavaScriptCall Signature 를 추론하여 동작한다

기존 JavaScript 프로젝트를 TypeScript 로 서서히 전환해갈 때 등, 두 파일이 섞여서 진행되는 경우가 있는데,

이 경우, JavaScript 파일에서 아래와 같은 주석(// @ts-check )을 최상단 첫째줄에 작성해주면, JavaScript 또한 TypeScript 컴파일러가 체크해준다

작성은 .js 로 하되, 체크만 해주는 것이다

// @ts-check

//... js codes

또, JSDoc 을 이용하면, 주석만으로 .js 파일에서 .ts 와 같이 타입 체크를 하며 작성할 수 있다

DefinitelyTyped

npm 의 여러 모듈 중에 TypeScript 의 지원을 할 수 있도록 구현해둔 것이 definitelyTyped 이다

이를 사용하기 위해서 해당 모듈이 definitelyTyped 에서 지원하고 아래와 같이 @types/모듈이름 의 형식으로 설치한다

npm i -D @types/node

@types/node 의 경우 node.js 에서 사용되는 기본 API 들에 대한 타입정의가 되어있다

최근에는 모듈자체를 설치하면 .d.ts 파일이 함께 설치되어 이미 타입 정의가 잘 된 경우도 많아졌다

References

tsconfig.json Property 알아보기

tsconfig.json Property 공식 Document