Typescript에서 인터페이스의 구현과 객체 리터럴(Object Literal) 할당

타입스크립트에서 인터페이스에는 미묘한 점이 존재합니다. 먼저 다음과 같은 인터페이스가 존재합니다.

interface Dip2K {
    action(): void
}

클래스로 구현해 보면 다음과 같습니다.

class Dip2KImpl implements Dip2K {
    alias: string = 'GISDEVELOPER'
    action() { console.log('인간은 행위로 정의된다.') }
}

새로운 구성요소로써 alias가 추가되었습니다. 네, 충분히 이해할 수 있습니다.

이제 객체 리터럴로 할당해 보겠습니다. 먼저 여기서 타입스크립트의 인터페이스에 대한 가장 핵심적인 목표는 ‘타입검사’라는 점을 떠올리셔야 합니다.

const Dip2KObj: Dip2K = {
    alias: 'GISDEVELOPER',
    action() { console.log('인간은 행위로 정의된다.') }
}

에러가 납니다. 에러 내용은 Object literal may only specify known properties, and ‘alias’ does not exist in type ‘Dip2K’ 입니다. 즉 인터페이스에는 없는 alias라는 속성은 지정할 수 없음을 나타냅니다. 타입스크립트의 인터페이스에 대한 가장 핵심적인 목표는 ‘타입검사’를 통과하지 못했기 때문입니다.

다음은 어떨까요?

class Dip2KDuck {
    alias: string = 'GISDEVELOPER'
    action() { console.log('인간은 행위로 정의된다.') }    
}

const Dip2KObj2: Dip2K = new Dip2KDuck()

가능할까요?

문제가 없습니다. 분명 서로 타입 이름이 다르므로 ‘타입검사’를 통과하지 못한 것으로 보이지만 ‘타입검사’를 통과합니다. 이유는 타입스크립트의 타입검사는 덕타이핑(Duck Typing) 방식이라는데 있습니다. 즉, 타입의 이름이 달라도 그 구성(필드, 매서드)가 같다면 동일 타입으로 본다는데 있습니다. 그럼 앞서 봤던 객체 리터럴은요? 객체 리터럴은 타입이 아닌 객체입니다. 그래서 ‘타입검사’를 통과하지 못하는 것입니다. 애매하죠? 사실 타입스크립트는 이해하기 매우 어려운 언어 중에 하나 입니다.

그럼 객체 리터럴에 대한 할당까지도 에러없이 처리하기 위해서는 어떻게 해야할까요? 다음처럼 인덱스 시그니쳐를 인터페이스의 선언에 추가하면 됩니다.

interface Dip2K {
    action(): void
    [prop:string]: any // 인덱스 시그니쳐
}

쌩뚱 맞은 인덱스 시그니쳐를 추가하는 것에 대해 불편함을 느낄 수 밖에 없는데요. 원래의 인터페이스에 인덱스 시그니쳐를 추가하지 않고도 좀더 나은 방식은 다음과 같습니다.

const Dip2KObj: Dip2K = <Dip2K>{
    alias: 'GISDEVELOPER',
    action() { console.log('인간은 행위로 정의된다.') }
}

프론트엔드 웹 페이지 JavaScript 개발 환경 만들기

기본 설정

# Node.js 설치

npm 명령 실행을 위함

# npm init -y

npm init는 package.json을 만들기 위한 명령이고 -y를 붙임으로써 별도의 입력 없이 기본 값으로 진행 시킴. package.json은 작성하고자 하는 프로젝트에 대한 설정 파일로 볼 수 있으며, 프로젝트 이름과 버전 등과 같은 설명과 프로젝트가 사용하는 라이브러리에 대한 정보 그리고 프로젝트 실행 등을 위한 명령에 대한 정보가 담겨있음. package.json 파일은 npm을 위한 파일임(VS.Code를 위한 것이 아님)

# npm install webpack webpack-cli --save-dev

# webpack.config.js 파일 생성 및 내용 작성

const path = require("path");
module.exports = {
    mode: "development",
    entry: "./src/index.js",
    devtool: "inline-source-map",
    module: {
        rules: [],
    },
    resolve: {
        extensions: [".js"],
    },
    output: {
        filename: "bundle.js",
        path: path.resolve(__dirname, "dist"),
    },
}

# src, dist 폴더 및 index.html(dist 폴더), index.js(src 폴더) 파일 생성

# index.html 코드 입력

...

<script src="bundle.js"></script> 

...

# index.js 코드 입력

console.log("Hello");

# package.json 파일의 “scripts” 파트에 “bundle”: “webpack” 입력

{
  ..
  "scripts": {
    ..
    "bundle": "webpack"
  },
  ..
}

# npm run bundle 실행

Typescript로 작성된 파일을 Javascript 파일로 트랜스파일링 시킴

자동 실행을 위한 설정

# package.json 파일의 “scripts” 파트에 “watch”: “webpack –watch” 및 “start”: “npm run bundle && npm run watch” 추가

{
  ..
  "scripts": {
    ..
    "bundle": "webpack",
    "watch": "webpack --watch",
    "start": "npm run bundle && npm run watch"
  },
  ..
}

# VS.Code에서 Live Server 확장 기능 설치

# npm run start 실행

이제부터 모든 Javascript 소스 코드들을 자동으로 bundle.js 파일 하나로 묶어줌

# index.html 열고 GO LIVE 활성화

TypeScript의 캔디코드

클래스 이름으로 해당 클래스의 인스턴스 생성

TypeScript에서 클래스 이름을 인자로 받아 해당 클래스의 인스턴스를 생성해 주는 코드입니다.

function create<T>(ClassName: { new(z: number): T }, i:number): T {
    return new ClassName(i);
}

위의 함수를 사용하는 코드는 다음과 같습니다.

class CLZ {
    i: number;
    constructor(i: number) {
        this.i = i;
    }
}

const o = create(CLZ, 100);
console.log(o.i);

실행 결과는 100

어떤 함수의 반환값 타입 얻기

어떤 함수가 있을 때.. 그 함수의 반환값에 대한 타입을 얻기 위한 코드입니다.

function func(x: number): string {
    return `value: ${x}`;
}

type K = ReturnType<typeof func>

위의 K 타입 별칭이 func 함수의 반환값에 대한 타입인데.. 결과는 string입니다.

배열 구성 요소의 타입 얻기

const MyArray = [
  { name: "Alice", age: 15 },
  { name: "Bob", age: 23 },
  { name: "Eve", age: 38 }
];

type Person = typeof MyArray[number];

사실 위의 코드 중 7번째의 number를 지정해 배열의 인덱스 값을 지정했는데, 이 곳에 어떤 숫자값이든 지정이 가능한데.. 이 부분이 애매모호해서 그냥 number로 지정하는 것으로 이해합니다. 여튼 위 코드에서 Person의 결과는 다음과 같습니다.

type Person = { age: number, name: string };

추론된 타입 얻기

TypeScript는 타입을 자동으로 추론하게 되는데 추론된 타입을 얻기 위해 infer를 사용합니다. 예를들어 배열을 구성하는 요소의 타입을 추론하고자 하는 예는 다음과 같습니다.

type Flatten<T> = T extends Array<infer E> ? E : T 
type Str = Flatten<string[]> // Str의 값은 string 타입
type Num = Flatten<number> // Num의 값은 number 타입
// 위의 코드는 다음과 동일함 : type Flatten<T> = T extends unknown[] ? T[number] : T

다른 예로 어떤 함수의 결과값의 타입에 대해 추론이 가능하다면.. 추론된 타입을 얻는 예는 다음과 같습니다.

type GetReturnType<T> = T extends (...args: never[]) => infer R ? R : never
type R1 = GetReturnType<() => number> // R1 값은 number 타입
type R2 = GetReturnType<(x: string) => string> // R2 값은 string 타입
type R3 = GetReturnType<(a: boolean, b: boolean) => boolean[]> // R3 값은 boolean[] 타입
function func(x: number): string { return `value: ${x}`; }
type R4 = GetReturnType<typeof func> // R4 값은 string 타입

생성자의 인자를 필드화 시키기(Parameter Properties)

클래스의 필드를 정의하는 코드를 작성하는 것은 때로 지루한데, 특히나 생성자로 초기화할 값을 받아 필드들의 값을 설정할때 그렇습니다. TypeScript는 다음처럼 이러한 지루함을 줄여줍니다.

class Params {
  constructor(
    public readonly x: number,
    protected y: number,
    private z: number
  ) {}
}

const a = new Params(1,2,3) // x, y, z는 각각 1, 2, 3으로 설정됨
console.log(a.x)
console.log(a.y) // ERROR
console.log(a.z) // ERROR
a.x = 100 // ERROR

인덱스 프로퍼티

interface User {
    name: string,
    [grade: number]: string,
    [key: string]: string,
}

const user: User = {
    name: "Dip2K",

    // [grade: number]: string로 인해 가능함
    0: "A",
    2: "B",
    5: "C",
    
    // [key: string]: string로 인해 가능함
    asdf: "adsf",
    axcv: "scvg",
}

console.log(user[2], user.asdf)

타입 도구 : Partial

Partial은 타입을 구성하는 항목 중 일부만으로 구성된 타입을 정의합니다. 아래의 코드를 이해한다면 TypeScript 프로그래머로써 중급 이상의 수준입니다.

interface Todo {
  title: string;
  description: string;
}
 
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return { ...todo, ...fieldsToUpdate };
}
 
const todo1 = {
  title: "organize desk",
  description: "clear clutter",
};
 
const todo2 = updateTodo(todo1, {
  description: "throw out trash",
});

console.log(todo2)

todo2를 출력해보면 {title: ‘organize desk’, description: ‘throw out trash’} 입니다.

리터널 타입으로 변경하기

handleRequest의 두번째 인자는 “GET” | “POST” 타입이므로 아래의 코드에서 req.method는 이 유니온 타입을 충족시키지 못한다고 타입스크립트는 해석합니다. 이에 대한 해결책은 아래 주석으로 막아 놓은 코드 2가지입니다.

const req = { url: "https://example.com", method: "GET" }
// SOLUTION I  : const req = { url: "https://example.com", method: "GET" } as const
// SOLUTION II : const req = { url: "https://example.com", method: "GET" as "GET"}
handleRequest(req.url, req.method)

function handleRequest(url: string, req: "GET" | "POST") {
  // .
}

any

어떤 타입의 값이든 할당을 허용하는 타입으로 TypeScript의 타입 시스템을 무력화시키는 타입입니다. 부득이한 경우 사용하되 점진적으로 이 타입의 사용을 제거해 나가야 합니다.

아래의 코드는 모두 정상적으로 처리됩니다.

let a: any = 10
a = "Hello"
a.TEST()
let b: number = a
console.log(a, b)

unknown

어떤 타입의 값이든 할당을 허용하지만 타입이 지정된 다른 타입에 할당등과 같은 혼용시 unknwon 타입 변수의 타입을 as 키워드를 사용하여 명확히 명시해줘야 합니다.

let c: unknown = "Hello"
// [ERROR] c.length 
if(typeof c === "string") {
  c.length
}
c = 10
let d: number = c as number

never

어떤 타입의 값도 할당되는 것을 허용하지 않는 타입입니다.

let e: never
// [ERROR] let f: never = 10

활용예로 2가지를 살펴볼 수 있는데, 첫번째는 실행될 수 없는 함수에 대한 반환값을 지정할때로 다음 코드와 같습니다.

const foo = (name: string): never => {
  throw new Error("Not Implemented")
}

또 하나는 generic 타입으로 지정되는 타입의 사용을 막기 위한 용도입니다.

type NotString<T> = T extends string ? never : T
// [ERROR] let f: NotString<string> = "Hello"
let g: NotString<boolean> = true

변경할 수 없는 배열 타입 만들기

배열의 구성 값들을 변경하거나 삭제할 수 없도록 하기 위한 코드입니다.

const a = [1, 2, 3]
a.push(4)

const b: readonly number[] = [1, 2 ,3]
b.push(4) // ERROR

JSZip에서 파일명이 한글인 파일을 압축한 파일 읽기

JSZip은 웹에서 zip 압축 파일을 생성하거나 풀 때 사용하는 라이브러리인데, 기본적으로 압축 파일 안의 파일 명이 한글일 때 파일 명이 깨진다. 그런데 이 문제를 해결할 수 있다. iconv 라이브러리를 사용하면 되고 JSZip은 내부적으로 iconv 라이브러리와 연계하여 압축 파일 안의 파일 명이 한글일때 문제점을 해결해 주는 API를 지원한다. 코드는 다음과 같다.

const zip = new JSZip();
zip.loadAsync(aBuf, {
    decodeFileName: function (bytes) {
        return iconv.decode(bytes, "euc-kr");
    }
}).then((zip) => {
    for(let file of Object.values(zip.files)) {
        console.log(file.name);
    }
});