타입스크립트랑 친해지기 1)

이병관·2022년 4월 12일
2
post-thumbnail

타입스크립트랑 제발 친해지기!

1) 자바스크립트와 타입스크립트 / 출처(테오님 벨로그)

이제 타입스크립트는 벗어날수 없는 거대한 파도와도 마찬가지입니다.

타입스크립트의 등장과 동시에 타입스크립트를 기반으로한 라이브러리들과 코드들이 점점 늘어가고있습니다.

초창기의 자바스크립트의 경우, 오늘날처럼 웹의 생태가 매우 거대해질것을 예측하지도, 상상하지도 못햇기에

단순한 형태의 스크립팅 언어로 시작되었습니다.

따라서 JAVA계열의 문법을 가져오면서, Public, Private접근제한자와 같은 설정이나,

Class와 같은 복잡성이나, 해당 변수가 Int형인지 String형식인지에 대한 Type에 대한 정의 역시 존재하지 않고

Prototype기반의 독특한 언어가 되었습니다

ProtoType
JavaScript는 클래스라는 개념이 없습니다.
그래서 기존의 객체를 복사하여(cloning) 새로운 객체를 생성하는 프로토타입 기반의 언어입니다.
프로토타입 기반 언어는 객체 원형인 프로토타입을 이용하여 새로운 객체를 만들어냅니다.
이렇게 생성된 객체 역시 또 다른 객체의 원형이 될 수 있습니다.
프로토타입은 객체를 확장하고 객체 지향적인 프로그래밍을 할 수 있게 해줍니다.

처음에는 형편없어 보였으나, AJAX의 조합으로 인한 DATA전송의 신세계, JQuery의 등장,

그리고 Node.js로 기어이 서버 사이드 언어로 탈바꿈되고, 결국 시간이 지나 웹 개발의 필수 언어가 되면서

자의든 타의든 자바스크립트의 성장 역시 급격하게 이루어지게 되었고,

JavaScript에 Class라는 개념을 도입하려 시도하거나, Python과 비슷하게 만들기 위해

CoffeeScript라는 JavaScript기반 새로운 언어를 만드려는 시도들이 존재했습니다.

이러한 시도들중, 가장 크게 성공하고 애용하는것은 바로 Typescript입니다.

물론 이전에도 타입스크립트처럼 자바스크립트를 좀더 '언어'스럽게 바꾸려는 시도가 존재했습니다.

바로 생소하실수도 있는 ES4에서 시도를 했는데요

허나 이렇게 구조화를 바꾸다 보니, 하위호환성에 대한 이슈와 급격하게 변한 언어를 따라가지 못할 문제가

발생햇습니다. 따라서 ES4는 폐기되고, 버전을 업그레이드할때엔

언어 자체를 바꾸는것 보단, 자바스크립트의 체계에서 조금씩 고쳐가는 방향으로 노선을 틀게됩니다.

http://channy.creation.net/blog/454

여튼 그렇게 자바스크립트는 발전에 발전을 거듭하며 유용한 언어가 되어가고있었으나,

자바스크립트 전체를 관통하는 가장 큰 문제는 역시 타입Static Typing입니다.

대규모 협업에 있어서 가장 중요한것은 여러가지가 있습니다.

이미 만들어진 데이터의 스키마 위에서 작업을 진행하는데,

작업중의 사소한 오타로 인한 에러, 그 에러는 런타임에서 발견할수있다는 문제는

생산성을 해치는 주 요인중 하나입니다.

그 때문에 자바스크립트의 동적타입 체계를 뒤집는게 아니라,

정적타입의 언어에서 '빌드전 오류를 검출'하는 장점을 가져온다면 어떨까? 라는 생각과,

C#과 유사한 형태로 만들되 자바스크립트의 원형을 살리면서

자신들의 IDE인 VS CODE에서 제대로 동작하는 언어를 만들자 라는 결심으로 인해

마이크로소프트는 자바스크립트의 슈퍼셋인 타입스크립트를 만들게됩니다.

2) 타입스크립트의 기본 타입

타입스크립트는 자바스크립트와 거의 동일한 타입을 지원합니다.

1) Boolean(불리언)

boolean은 true/false 값을 나타냅니다.

let isDone: boolean = false;

2. Number(숫자)

TS의 모든 숫자는 부동 소수 값입니다.

따라서number라는 타입을 붙입니다. 2진수, 8진수, 10진수, 16진수도 지원합니다.

let binary: number = 0b1010;
let octal: number = 0o744;
let decimal: number = 6;
let hex: number = 0xf00d;

3. String(문자열)

텍스트 데이터 타입을 string으로 표현하며,

자바스크립트처럼 (")나 (')로 문자열 데이터를 감싸는 데에 사용합니다

 템플릿 문자열이나 백틱을 사용하면 여러 줄의 문자열을 작성할 수 있고,

 ${expr} 같은 형태로 표현식을 포함할 수도 있습니다.

let color: string = "blue";
color = 'red';
let name: string = `TypeScript`;
let age: `8`;
let sentence: string = `Hello, my name is ${ Name }.`;
let sentence2: string = "Hello, my name is " + Name + ".\n\n" +"I want to be " + 
(generation + 1) + " year Programmer.";

4. Array(배열)

배열Array은 두가지 방법으로 작성할 수 있습니다.

// 1. 배열 요소들을 나타내는 타입 뒤에 `[]` 쓰기
let list: number[] = [1,2,3];

// 2. 제네릭 배열 타입 - `Array<elemType> 
let list: Array<number> = [1,2,3];

5. Tuple(튜플)

튜플은 요소의 타입과 개수가 고정된 배열을 표현할 때 사용합니다.

(단, 정의한 요소들의 타입이 모두 같을 필요는 없습니다.)

let x: [string, number]; // 튜플 타입으로 선언
x = ["name", 10]; // 성공, 튜플선언과 타입이 같습니다
x = [10, "hello"]; // 에러, 튜플선언과 타입이 다릅니다.

// 정해진 인덱스에 위치한 요소에 접근하면 해당 타입이 나타납니다. 
console.log(x[0].substring(1)); // 성공
console.log(x[1].substring(1)); // 에러 (number에는 substring 내장함수가 존재하지않습니다.)

// 정해진 인덱스 외에 다른 인덱스에 있는 요소에 접근 시, 에러가 발생합니다.
x[3] = "world"; // 에러, [index: 3]에 해당되는 요소가 없습니다.
console.log(x[4].toString()); // index: 5에 해당되는 요소가 없습니다.

6. Enum(열거형)

enum은 열거형 변수로 정수를 하나로 합칠 때 편리한 기능입니다. 

임의의 숫자나 문자열을 할당할 수 있으며 하나의 유형으로 사용해서 버그를 줄일 수 있습니다.

해당 타입에 대한 좀더 상세한 설명은 아래에서 설명하겠습니다.

// 0부터 번호를 시작합니다.
enum Color {Red, Green, Blue}
let c: Color = Color.Green;

// 0이 아닌 1로 시작하게 합니다.
enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];
console.log(colorName); // 값이 2인 'Green'이 출력됩니다.

// 모든 값을 수동으로 설정합니다
enum Color {Red = 1, Green = 3, Blue = 6}
let c: Color = Color.Green;
console.log(c); // Green에 3을 할당했습니다, 따라서 3이 출력됩니다.

7. Any

알지 못하는 타입을 표현해야 할 때,

(사용자로부터 받은 데이터나 서드 파티 라이브러리 같은 동적인 컨텐츠로 부터 온 값들)

타입 검사를 하지 않는 any를 사용합니다.

let notSure: any = 4;
notSure = "maybe a string instead"; //any를 할당하여 어떤 타입이든 들어올수 있어 할당됩니다
notSure = false; //any를 할당하여 어떤 타입이든 들어올수 있어 할당됩니다

any 타입은 컴파일 중 타입 검사를 할 수도 있고 안할 수도 있습니다.

Object로 선언된 변수들은 어떤 값이든 그 변수에 할당할 수 있게 해주는 점에서 

any와 비슷한 역할을 할 수 있으나, 실제로는 메서드가 존재하더라도 임의로 호출할 수는 없습니다.

let notSure: any = 4;
notSure.ifItExists(); // 성공, ifItExists는 런타임엔 존재할 것이라 생각합니다.
notSure.toFixed(); // 성공, toFixed는 존재할것이라 생각합니다.

let prettySure: Object = 4;
prettySure.toFixed(); // 오류: 프로퍼티 'toFixed'는'Object'에 존재하지 않음

또한 타입을 모르기때문에 자동완성 역시 지원하지 않습니다.

그리고 Any타입의 일부만 알고 전체는 알지 못할 때 유용합니다.

any와 unknown

타입스크립트 3.0부터 unknown 타입이 도입되었습니다.

any는 기본적으로 모든 타입 체크를 무력화 시키기에 사용하기에 매우 편리합니다.

허나 이는 다른말로 타입을 엄격하게 검사 또는 체크하지 않기 때문에

타입 구조에 대한 가정이 런타임 코드에 의해서만 실패하게 만들기 때문에 버그를 잡기 힘듭니다.

let val: any = 10;

console.log(typeof val); // number
console.log(val.length); // undefined

빌드 과정에서는 오류가 없지만 런타임시에는 val은 number타입이기에 undefined라는 값이 도출되었습니다

이처럼 모든 타입체크를 무력화 하기때문에 종종 문제가 발생할 수 있습니다.

let valName: unknown = 10;
let valStr: unknow = 'Test';

console.log(valName.length); //빌드과정 ERROR
console.log(valStr.length;// 빌드과정 

unknown 타입으로 변수를 정의할 경우, 변수의 타입은

unknown이기에 어떤 값이든 들어올수있으니, 엄격하게 검사해달라. 라는 요청입니다.

let valName: unknown = 10;
let valStr: unknown = 'Test';

if (typeof valName === 'number') {
  console.log(valName.length)
}
if (typeof valStr === 'string') {
  console.log(valStr.length)
}

타입검사는 정상적이지만, valName이 number 타입이기에 length를 사용할수 없다고 개발자에게 경고합니다.

정리하자면 다음과 같습니다

any:

  • 모든 타입을 허용합니다
  • 느슨한 검사이기 때문에 개발에는 문제가 없을수 있습니다, 허나 개발후 예기치 못한 문제가 발생할 수 있습니다

unknown

  • 모든 타입을 허용합니다.
  • 프로퍼티 또는 연산의 경우엔 컴파일러가 체크후, 문제가 될 소지가 있는 부분을 체크합니다

8. Void

void는 어떤 타입도 존재할 수 없음을 나타냅니다.

any와 약간 다른의미로써,  

void는 보통 변수에는 undefined와 null만 할당하고,

함수에는 반환 값을 설정할 수 없는 타입입니다.

function IloveU: void {
  console.log("I love u all");
}

9. Never

함수의 끝에 절대 도달하지 않는다는 뜻의 타입입니다

// 이 함수는 절대 함수의 끝까지 실행되지 않는다는 의미
function neverEnd(): never {
  while (true) {

  }
}

그래서 정확하게 Never이 뭔데?...

never를 정확히 이해하기 위해선 타입이란 무엇이고, 어떤 역활을 수행하는지 알아야합니다.

타입의 좀더 명확한 뜻은, 가능한 값의 집합입니다.

예를들어 number타입은 무한하게 가능한 모든 숫자형의 집합이고,

string타입은 무한하게 가능한 모든 문자열의 집합입니다.

그래서 변수 타입을 string으로 지정할시, 해당 변수는 오직 무한하게 가능한 모든 문자열의 집합에 속한 원소의 값만 가질수 있다는 뜻힙니다.

let foo: string = 'foo'
foo = 3 // 숫자는 문자열 타입에 들어가지 않습니다

never type은 공집합입니다.

집합에 어떠한 값도 존재하지 않기 때문에, 즉 어떠한 존재 자체도 없다는 말이기에

any 타입의 값을 포함한 어떠한 값조 가질수 없습니다.

declare const any: any
const never: never = any // 공집합에는 어떤것도 존재하지않습니다. any타입은 never에 속하지 않습니다

그렇다면 대체 Never는 왜 존재할까요?

'불가능' 을 명시하는 타입

무언가 존재하지 않음을 나타내는 숫자는 바로 0입니다.

이처럼 불가능을 나타내는 타입이 필요할 때가 존재합니다.

타입스크립트에서는 불가능할때를 나타내기 위해선 다양한 방법을 통해 알려줍니다.

  • 값을 포함할 수 없는 빈 타입
    • 제네릭과 함수에서 허용되지 않는 매개변수
    • 호환되지 않는 타입들의 교차 타입
    • 빈 합집합(무의 합집합)
  • 실행이 끝날 때 호출자에게 제어를 반환하지 않는 함수의 반환 타입
    • 예) Node의 process.exit
    • void는 호출자에게 함수가 유용한 것을 반환하지 않는다는 것이므로 혼동하지 않도록 한다.
  • 절대로 도달할수 없을 esle 분기의 조건 타입
  • 거부된 프로미스에서 처리된 값의 타입
const p = Promise.reject('foo') 
// const p: Promise<never>

3) 인터페이스

인터페이스란 상호 간에 정의한 약속 또는 규칙을 의미합니다.

즉 특정 변수에 대한 특정한 규칙을 정의할 수 있는, 두개의 시스템 사이를 상호작용 할수있게 도와주는

조건, 규약이라 볼 수 있습니다.

인터페이스는 타입이며 컴파일 후에 사라집니다.

추상 클래스는 선언과 구현이 모두 존재하지만 인터페이스는 선언만 존재하며,

멤버 변수와 멤버 메서드를 선언할 수 있지만 접근 제한자는 설정할 수 없습니다.

인터페이스를 통해 다음과 같은 조건들을 약속할 수 있습니다.

  • 객체의 스펙(속성과 속성의 타입)
  • 함수의 파라미터
  • 함수의 스펙(파라미터, 반환 타입 등)
  • 배열과 객체를 접근하는 방식
  • 클래스
interface Human { //타입 체크용 인터페이스, 인터베이스는 에러를 막기위해 컴파일 되지 않는다. 
   name: string, 
   age: number, 
   gender: string
}

const whoIAm: Human = {
  name:'Lee',
  age: 25,
  gender: 'Male'
}
console.log(whoIAm.name) // Lee

그런데 여기서 의문점이 들 수 있습니다.

 바로 나중에 설명할 Type Alias와 Interface 둘 중 어느 것을 사용해 타입을 정의할까?’ 라는 문제입니다.

3-1) 선택적 옵션 설정

인터페이스를 사용할때 인터페이스에 정의된 모든 속성을 이용하지 않아도 됩니다

interface 인터페이스_이름 {
  속성?: 타입;
}

원하는 속성의 끝에 ? 를 붙여 속성이 존재할수도 안할수도 있다는것을 명시할수 있습니다.

interface Human { //타입 체크용 인터페이스, 인터베이스는 에러를 막기위해 컴파일 되지 않는다. 
   name: string, 
   age?: number, 
   gender?: string
}

const whoIAm: Human = {
  name:'Lee',
}
console.log(whoIAm.name) // Lee

Human이라는 인터페이스에 agegender를 옵션으로 설정했기 때문에

객체에 해당 속성들이 존재하지 않아도 빌드전 에서 오류가 나지 않습니다.

3-2) 읽기 전용 속성

인터페이스로 객체를 처음 생성할 때만 값을 할당하고,

그 이후에는 해당 속성을 변경할수 없는것을뜻합니다.

다음과 같이 readonly 속성을 붙입니다

interface Human { //타입 체크용 인터페이스, 인터베이스는 에러를 막기위해 컴파일 되지 않는다. 
   readonly name: string, 
   age?: number, 
   gender?: string
}

const whoIAm: Human = {
  name:'Lee',
}
console.log(whoIAm.name) // Lee
whoIAm.name = 'Kim' // ERROR

읽기 전용 배열

배열을 선언시에는 제네릭 타입을 사용하여 선언할수도 있으나,

다음과 같이 읽기 전용 배열로 만들면 선언한 시점에만 값을 정의할 수 있습니다.

let arr: ReadonlyArray<number> = [1,2,3];

arr.splice(0,1); // error
arr.push(4); // error
arr[0] = 100; // error
arr = [10, 20, 30]; // error

3-3) 함수 타입

인터페이스를 사용할때는 인자의 타입과 반환값의 타입 역시 정의할 수 있습니다.

interface KeyBoard {
    (pitch: string, weight: number): boolean;
}
const checkCreatedKeyboard: KeyBoard = (p: string, w: number): boolean => w < 10;
console.log(checkCreatedKeyboard('took', 11)); // false;
console.log(checkCreatedKeyboard('tok', 3)); // true

키보드의 인터페이스는 눌림정도pitch와 누르는 힘weight 을 정의하고 ,

누르는 힘이 10이하일때만 사용이 가능 하도록 정의했습니다.

인터페이스에서 정의한 속성부분과 구현부분의 속성부분이 반드시 동일할 필요는 없습니다.

pitch를 p로, weight를 w로 정의하여 받을수 있음을 확인함으로 알 수 있습니다.

3-4) 클래스 타입

또한 클래스에서도 인터페이스를 사용 할 수 있습니다. 이는 implements라는 키워드를 통해 구현할 수 있습니다.

여기서 주의할 점은

interface ClockInterface {
    currentTime: Date;
}
class Clock implements ClockInterface {
} 
//Class 'Clock' incorrectly implements interface 'ClockInterface'.
 //Property 'currentTime' is missing in type 'Clock' but required in type 'ClockInterface'.

implements 키워드가 존재할 경우 해당 인터페이스를 무조건 구현해야만 합니다.

interface ClockInterface {
    currentTime: Date;
}
class Clock implements ClockInterface {
    public currentTime: Date;
    // private _currentTime: Date; // Error!
}
const digital: ClockInterface = new Clock();

또한 인터페이스를 구현하는 클래스는 public만 사용할 수 있는데 그 이유는

private로 선언시, 접근할 수 없어 구현이 되었는지 안 되어있는지 알 수 없기 때문입니다.

3-5) 인터페이스 확장

인터페이스도 클래스처럼 extends 키워드를 통해 확장할 수 있습니다.

인터페이스를 분리함으로써 재사용성이 늘어나게 됩니다

interface DOM {
    display: string;
    tag: string;
}
interface TextNode extends DOM {
    text: string;
}
interface InputNode extends DOM {
    type: string;
}
const textNode: TextNode = {
    display: 'inline',
    tag: 'text',
    text: 'heecheolman',
};
const InputNode: InputNode = {
    display: 'inline-block',
    tag: 'input',
    type: 'button',
};

또는 여러개의 인터페이스를 상속받을 수 있습니다.

interface Person {
  name: string;
}
interface Drinker {
  drink: string;
}
interface Developer extends Person {
  skill: string;
}
let fe = {} as Developer;
fe.name = 'josh';
fe.skill = 'TypeScript';
fe.drink = 'Beer';

3-6) 하이브리드 타입

자바스크립트의 속성에는 함수 역시 포함될 수 있습니다.

그것을 응용하여 다음과 같이 함수 타입이면서 객체 타입을 정의할 수 있는 인터페이스를 만들 수 있습니다

interface CraftBeer {
  (beer: string): string;
  brand: string;
  brew(): void;
}

function myBeer(): CraftBeer {
  let my = (function(beer: string) {}) as CraftBeer;
  my.brand = 'Beer Kitchen';
  my.brew = function() {};
  return my;
}

let brewedBeer = myBeer();
brewedBeer('My First Beer');
brewedBeer.brand = 'Pangyo Craft';
brewedBeer.brew();

인터페이스와 클래스

인터페이스와 클래스는 서로 무언가 비슷하면서도 틀린 느낌이 듭니다.

따라서 간단하게 두 개를 짚고 넘어가며 비교해보겠습니다.

인터페이스

인터페이스는 ES6가 지원하지 않는 타입스크립트만의 특징입니다.

인터페이스는 타입이며 컴파일 후에 사라지고 선언만 존재하며,

멤버 변수와 멤버 메서드를 선언할 수 있지만 접근 제한자는 설정할 수 없습니다.

클래스와 달리 interface는 TypeScript의 컨텍스트 내에서만 존재하는 가상 구조입니다. TypeScript 컴파일러는 타입 체크 목적으로만 인터페이스를 사용합니다. 즉 인터페이스는 JS파일이나 코드로 변환되지 않습니다.

클래스

클래스는 ES6에서 JavaScript 생태계에 공식적으로 도입되었습니다.

객체지향 프로그래밍은 커다란 문제를 클래스라는 단위로 나누고 클래스 간의 관계를 추가하면서 코드 중복을 최소화하는 개발 방식입니다. 클래스는 객체지향 프로그래밍 그 자체이며 클래스 간의 관계는 상속이나 포함 관계를 고려해 추가합니다.

특징:

인터페이스 :

  • 커스텀 Type을 정의 할 수 있습니다
  • 함수에 전달되는 인자와 같이 형식을 고정할 수 있습니다
  • 형식이 맞지 않으면 유효성 에러를 내뱉습니다
  • 추상클래스에 사용됩니다,
    • 메소드 형태만 선언하여 인터페이스를 정의하고, 클래스를 정의 할 때 implements 키워드를 사용하여 추상적으로 정의된 클래스의 함수를 정의할 수 있습니다.

클래스:

  • 객체를 생성하기 위한 팩토리로 사용됩니다.

객체의 모양과 동작을 선언하고 클래스를 만들때 실행 가능한 함수와 정의된 속성을 통해 객체를 만들어냅니다.

사용처:

인터페이스:

  • 코드에서 두곳 이상의 파일 또는 함수에서 사용될때, 객체의 속성과 함수에 대한 정의가 필요할때 사용합니다.
  • 단순히 타입 체크를 용이하게 하기 위해선 인터페이스가 효율적입니다

클래스:

  • 함수를 포함한 객체를 만들기 위해 초기화를 시켜줄 필요가 있다면 클래스가 유용합니다.
  • 타입체크 뿐만 아니라 constructor를 사용하여 함수의 구현까지 정의하고 싶다면 class가 효율적입니다.
  • (다만 인터페이스를 Implements를 사용하여 Class에 포함시킬수도 있습니다.)

4) Enum

TypeScript enum은 JavaScript에는 없는 개념으로,

변수, 배열, 혹은 객체와 비슷해보이기도 합니다.

그냥 변수, 배열, 객체를 써도 될 것 같은데, 굳이 enum을 쓰는 이유가 뭘까요?

4-1) 추상화

다국어를 지원하는 기능을 위해,

언어 코드를 저장할 변수를 만들때를 생각해 본다면

const korean = 'ko'
const english = 'en'
const japanese = 'ja'
const chinese = 'zh'
const spanish = 'es'

// 이렇게 하면 언어 코드가 위아래에 중복되고
type LanguageCode = 'ko' | 'en' | 'ja' | 'zh' | 'es'

// 이렇게 하면 코드가 너무 길어집니다

type LanguageCode = typeof korean | typeof english | typeof japanese | typeof chinese | typeof spanish
const code: LanguageCode = korean

코딩을 하다보면 어떤 언어를 지원하기로 했는지도, 특정 국가 코드가 어떤 언어를 가르키는지도 힘든 상황이 존재할것입니다.

이처럼 여러개의 상수를 두어 해결할 수 있지만, 깔끔한 느낌이 들지 않습니다.

이런 경우에 enum을 사용해

 리터럴의 타입과 값에 이름을 붙여서 코드의 가독성을 크게 높일 수 있습니다.

export enum LanguageCode {
  korean = 'ko',
  english = 'en',
  japanese = 'ja',
  chinese = 'zh',
  spanish = 'es',
}

// LanguageCode.korean === 'ko'
// (의미상) LanguageCode === 'ko' | 'en' | 'ja' | 'zh' | 'es'
const code: LanguageCode = LanguageCode.korean

짧은 코드로 타입의 범위도 좁히고, 가독성도 높일 수 있게 되었습니다.

4-2) Enum is Object, But...

TypeScript enum은 그 자체로 객체이기도 합니다.

const keys = Object.keys(LanguageCode) // ['korean', 'english', ...]
const values = Object.values(LanguageCode) // ['ko', 'en', ...]

다만 객체와는 약간의 차이가 존재합니다.

1. 객체는 (별다른 처리를 해주지 않았다면) 속성을 자유로이 변경할 수 있는데 반해,

 enum의 속성은 변경할 수 없습니다.

2. 객체의 속성은 (별다른 처리를 해주지 않았다면)

리터럴의 타입이 아니라 그보다 넓은 타입으로 타입 추론이 이루어집니다.

(타입 추론에 관한 설명은 아래에서 좀더 자세히 설명 하겠습니다.)

enum은 항상 리터럴 타입이 사용됩니다.

3. 객체의 속성 값으로는 JavaScript가 허용하는 모든 값이 올 수 있지만, 

enum의 속성 값으로는 문자열 또는 숫자만 허용됩니다.

정리하자면, 같은 ‘종류’를 나타내는 여러 개의 숫자 혹은 문자열을 다뤄야 하는데,

각각 적당한 이름을 붙여서 코드의 가독성을 높이고 싶다면 enum이 유용하고,

 그 외의 경우엔 변수, 상수, 배열, 객체 등을 적절히 사용하면 될것입니다.

- Keyof의 주의점

이넘이 런타임 시점에서는 실제 객체지만 keyof를 사용할 때 주의해야 합니다.

일반적으로 keyof를 사용해야 되는 상황에서는 대신 keyof typeof를 사용해야합니다.

enum LogLevel {
  ERROR, WARN, INFO, DEBUG
}

// 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
type LogLevelStrings = keyof typeof LogLevel;

- 리버스 맵핑

리버스 매핑은 숫자형 이넘에만 존재하는 특징입니다.

이넘의 키(key)로 값(value)를 얻을 수 있고 값(value)로 키(key)를 얻을 수도 있습니다.

다만 문자형 이넘에는 리버스 맵핑이 존재하지않습니다

enum Enum {
  A
}
let a = Enum.A; // 키로 값을 획득 하기
let keyName = Enum[a]; // 값으로 키를 획득 하기

4-3) Enum의 종류

- 숫자형 Enum

숫자형 이넘은 아래와 같이 선언합니다.

enum NumberFamilly {
  One = 1,
  Two,
  Three,
  Four
}
///
One - 1
Two - 2
Three - 3
Four -4

위와 같이 숫자형 이넘을 선언할 때 초기 값을 주면 초기 값부터 차례로 1씩 증가하고,

초기값을 선언하지 않으면 0부터 차례로 증가합니다.

- 문자형 Enum

문자형 이넘은 이넘 값 전부 다 특정 문자 또는 다른 이넘 값으로 초기화 해줘야 합니다.

enum NumberFamilly {
  One = 'ONE',
  Two = 'TWO',
  Three = 'THREE',
  Four = 'FOUR'
}

ENUM, 너는 Tree-Shaking 되지 않아

enum은 편리한 기능이지만 TypeScript가 자체적으로 구현했기 때문에 발생하는 문제가 있습니다.

아래와 같이 TypeScript 코드를 작성한 경우를 생각해 보겠습니다.

export enum PlayerList {
  Warrior,
  mage,
  archor
}

위의 Enum을 JS로 변환시킨 코드는 다음과 같습니다.

JavaScript에 존재하지 않는 것을 구현하기 위해 TypeScript 컴파일러는 IIFE(즉시 실행 함수)를 포함한 코드를 생성합니다.

그런데 Rollup과 같은 번들러는 IIFE를 ‘사용하지 않는 코드’라고 판단할 수 없어서

 Tree-shaking이 되지 않습니다. 

결국 PlayerList를 import하고 실제로는 사용하지 않더라도 최종 번들에는 포함되는 것입니다.

타입추론 에러

또 나중에 코드 상에서 사용할때에도 불편한점이 존재할수 있습니다.

 enum의 value에 해당하는 string 값과 enum 타입이 일치하지 않다고 판단하는 타입에러가 발생할 수 도있습니다.

Union Types!

그렇기에 Union Types을 타입스크립트 공식 문서에서도 사용하라 소개하고 있습니다.

//객체 형식
export enum PlayerList {
  Warrior: 'Warrior',
  mage : 'mage',
  archor: 'archor',
} as const;
type RealPlayerList = typeof PlayerList[keyof typeof PlayerList];
// Warrior | mage | archor

//배열 형식
const PlayerList = ['Warrior', 'mage', 'archor'] as const;
type RealPlayerList = typeof PlayerList[keyof typeof PlayerList]

keyof는 Object의 key들의 lieteral 값들을 가져오는 키워드입니다.

as const는 객체나 배열도 const로 선언한 원시 값의 타입처럼,

리터럴 타입의 추론 범위를 줄이고 값의 재할당을 막아줍니다(readonly)

위 코드에서 as const를 사용하지 않으면

object의 value 값이 string으로 추론되지만

사용하면 'Warrior', 'mage... 처럼 각각의 명시된 문자열 자체로 타입이 지정됩니다.

이제 위와 같은 방법으로 union 타입을 사용하면 tree shacking 의 문제도 해결이 됩니다.


5) 제네릭 Generic

제네릭은 재사용성을 높여주기 위해 사용하는 문법중 하나입니다.

예를 들어 숫자타입의 arg를 그대로 돌려주는 함수를 작성했다 가정했을때,

function identity(arg : number) : number {
	return arg;
}

다음과 같이 작성 할 수 있을 것입니다.

허나 다른 로직을 처리하기위해 string또는 boolean 역시 처리를 해야한다면 어떻게 작성해야할까요

function identityNum(arg : number) : number {
	return arg;
}

function identityString(arg : string) : string {
	return arg;
}

function identityBoolean(arg : boolean) : boolean {
	return arg;
}

다음과 같은 형식으로 작성한다는건 조금 비효율적인 코딩이 될 수 있습니다.

그게 아니라면 Any를 사용할수도 잇습니다.

function identity(arg : number) : number {
	return arg;
}


function identityAny(arg : any) : any {
	return arg;
}

const str = identity("test"); // <- Error 
const strG = identityAny("test"); // <- Ok ✔

허나 Any로 받을 경우엔 원래 들어온 객체의 기본 타입을 잃어버리는 단점을 가지고있습니다.

게다가 타입의 종류를 모르기 때문에 Any로 설정했을땐, 프로그램 실행 도중 문제가 발생 할 가능성이 존재합니다.

따라서 이럴때는 바로 제네릭을 사용할 수 있습니다.

제네릭은 타입을 마치 함수의 파라미터처럼 사용할수 있습니다.

또한 타입을 미리 알 수 있기 때문에 타입에서 벗어난 함수와 같이 사전에 문제가 되는 부분을 미리 검수할수 있다는 장점역시 존재합니다.

제네릭을 사용하게 된다면 타입 변수가 추가되어 사용자가 작성한 타입을 따르게 됩니다.

function identity<T>(arg: T): T {
    return arg;
}

//일반적인 호출 ( 타입 인수 추론을 사용한 호출 )
const str : string = identity("test");
const num : number = identity(100);

// 별개로 호출( 타입을 포함한 호출 )
let extra : string = identity<string>("extra");

구현은 꺾쇠(<>)를 사용하여 타입을 명시하고, 인자에도 타입을 명시하고, 반환값에도 타입을 명시하는 형태입니다.

function identity<Type>(arg: Type): Type {
    return arg;
}

또한 T 뿐만 아니라 Type 등 어떤 문자이든 가능합니다.

다만, 이넘(enum)네임스페이스(namespace)는 제네릭으로 생성할 수 없습니다.

정리하자면 제네릭을 사용할 때, 타입 파라미터는 하나 이상 복수개를 정의할 수 있으며,

이 타입 파라미터는 입력 파라미터, 리턴 타입, 함수 본문 등 어느 곳에서도 사용될 수 있습니다.

특별한 제약(type contraint)을 가하지 않는 한, 타입 파라미터는 모든 타입을 받아들일 수 있으며,

따라서 모든 타입에 적용될 수 있는 제네릭 함수을 작성해야 합니다.

앞서 말한것과 같이 제네릭은

모든 타입을 받아들인다는 점에서 입력파라미터 혹은 리턴에 any 타입을 쓰는 것과 비슷한 점이 있지만,

제네릭은 사용시 타입파라미터를 구체적으로 정의하기 때문에 구체적인 입력 타입,

리턴타입을 정의한 것과 같이 타입 체킹을 하게 된다는 장점이 있습니다.

5-1) 제네릭 클래스와 인터페이스

제네릭은 제네릭 함수와 마찬가지로 인터페이스나 클래스에 적용될 수 있습니다.

인터페이스와 클래스에서 각각 타입파라미터 T 를 받아들이고 있고,

이를 클래스와 인터페이스 내에서 사용할 수 있습니다.

타입파라미터 T는 속성과 메서드, 입력파라미터와 리턴타입 등에 모두 사용할 수 있습니다.

//제네릭 인터페이스
interface PrintFn {
  <T>(arg: T): T
}

function print<T>(arg: T): T {
  console.log('print function:', arg)
  return arg
}

const printFn: PrintFn = print

printFn(1234)

클래스도 위의 제네릭 인터페이스와 마찬가지로 제네릭을 사용하여 제네릭 클래식으로 만들수 잇습니다.

//제네릭 클래스
class PlusTwoGeneric<T> {
  plus: (a: T, b: T) => T
}

const plusTwoString = new PlusTwoGeneric<string>()
const plusTwoNumber = new PlusTwoGeneric<number>()

plusTwoString.plus = (a: string, b: string) => {
  return a + b
}

plusTwoNumber.plus = (a: number, b: number) => {
  return a + b
}

const strOneAndTwo = plusTwoString.plus('1', '2')
const numOneAndTwo = plusTwoNumber.plus(1, 2)

console.log(strOneAndTwo) // '12'
console.log(numOneAndTwo) // 3

plus의 함수에 인자가 T타입을 가지고, 반환값의 타입 역시 T를 반환합니다.

이를 이용해 새로운 인스턴스를 만들고

문자가 들어간 plusTwoString 인스턴스는 문자열을 반환,

숫자가 들어간 plusTwoNumber 인스턴스는 들어온 숫자값을 계산하여 반환하는것을 볼 수 있습니다.

5-2) 제네릭 제약 조건

특정 타입들로만 동작하는 제네릭 함수를 만들고 싶을 때 사용합니다.

예를 들자면

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // 오류: T에는 .length가 없다.
    return arg;
}

다음과 같은 함수는  arg의 프로퍼티 .length에 접근하기를 원했지만,

컴파일러는 모든 타입에서 .length 프로퍼티를 가지는 것을 증명할 수 없으니 에러를 발생시켰습니다.

// .length를 가진 인터페이스를 생성합니다.
interface Lengthwise {
    length: number;
}

// extends 키워드로 표현한 인터페이스를 이용하여 제약사항을 명시합니다.
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  
// 이제 .length 프로퍼티가 있는 것을 알기 때문에 더 이상 오류가 발생하지 않습니다.
    return arg;
}

5-3) 제너릭 타입 변수도 변수!

IDE들이 기술적인 정보(타입)를 보여주기 시작하면서 변수 이름에 기술적인 정보를 포함하는 것은 피하기 시작했습니다.

(ex: const strName = 'Daniel' 대신 const name = 'Daniel' )

또한, 한 문자 변수 이름은 변수가 선언된 부분을 보지 않고서는 그 의미를 파악하기 힘들기 때문에

코드에 대한 이해가 적을 경우 해당 변수를 파악하는것도 힘들수 있습니다.

물론 공식 문서에서 역시 제네릭은 와 같이 한문자로 작성 하긴 하나, 만일 좀더 명확하게 변수 이름을 작성한다면 더 생산적인 제네릭을 만들수 있을것입니다.

///제네릭 T와 제네릭 변수명을 준것의 차이

function head<T> (arr: T[]): T | undefined {
  return arr[0]
}

///////

function head<Element> (arr: Element[]): Element | undefined {
  return arr[0]
}

6) 타입 추론(Type Inference)

타입 추론이란 타입스크립트가 코드를 해석해 나가는 동작을 의미합니다.

let test = 3; 
test = "ASD"; 

이 소스를 자바스크립트 상에서 구동한다면 에러 없이 test 라는 변수에 ASD 라는

문자열이 저장 될 것입니다.

하지만 타입스크립트에서는 첫번째 줄에서 3이라는 숫자를 할당했기에,

타입스크립트에서는 test 의 타입을 숫자로 간주하고, 문자열을 저장할 수 없게 됩니다.

test에 대한 타입을 따로 지정하지 않더라도, test는 string type으로 간주되었습니다.

이러한 과정을 타입 추론이라고 부릅니다.

타입추론은 다음과 같은 상황에서 발생합니다.

  1. 변수를 선언하거나, 초기화할때,
  2. 변수, 속성, 인자의 기본값, 함수의 반환 값등을 설정할때.
function getB(b = 10) {
   let c = 'hi';
   return b + c;
 }

다음과 같은 함수에서는 어떻게 타입 추론이 되는지 생각해 보겠습니다.

b에는 타입 대신 10을 할당햇습니다. 따라서 b라는 값이 없다면

b는 기본적으로 10이란 뜻입니다.

즉 만일 return b + c 가 아니라 return b만 햇을경우엔 10을 반환할것입니다.

하지만

c의 타입은 문자열입니다.

자바스크립트에서 10 + '10'을 햇을 경우에는 10을 문자열로 인식하고 '1010'을 리턴할것입니다.

따라서 숫자와 문자열을 더해 리턴해야하므로, 리턴값은 string이라 추론하게됩니다.

6-1) 가장 적절한 타입(Best Common Type)

여러 타입이 들어가있는 배열이 존재한다 가정했을때,

let arr = [1, 2, true, 'abc'];

타입스크립트에서는 이런 배열들에 최대한 많이 적용이 될 수 잇는 형태로 타입을 추론합니다.

이를 최적 공통 타입(Best Common Type)을 찾아 추론한다 합니다.

6-2) 문맥상 타이핑

타입스크립트는 또한 경우에 따라 '다른 방향' 쪽으로 타입을 추론하기도 합니다.

이를 문맥상 타이핑 (Contextual Typing)이라 부릅니다.

window.onmousedown = function(mouseEvent) {
  console.log(mouseEvent.a);
};

다음과 같은 함수를 가정했을때, Window 인터페이스의 onmousedown속성은 다음과 같이 정의되있습니다

interface MouseEvent {
  /* ... */
  /* button 속성 없음! */
}

interface Window {
  /* ... */
  onmousedown: (event: MouseEvent) => void;
}

따라서 타입스크립트는 우변의 함수가 (event: MouseEvent) => void 타입일 것이라고 추론합니다.

이 때 함수 내부에서 event.a 속성에 접근하는데,

a 속성은 MouseEvent 타입에 존재하지 않으므로 타입 에러가 발생합니다.

만약 타입 표기가 주어졌다면 문맥 상의 타입은 무시합니다.

예를 들어, 다음과 같이 mouseEvent 매개변수의 타입을 표기해주면 위의 에러는 사라집니다.

window.onmousedown = function(mouseEvent: any) {
  console.log(mouseEvent.a);
};

7) 타입 호환(Type Compatibility)

타입 호환성은 타입스크립트 코드에서 특정 타입이 다른 타입에 잘 맞는지를 의미합니다.

interface Restaurant {
	name: string;
	star: number;
}

let pinetree: Restaurant;  // pinetree 변수 선언

let pinetreeWithAddress = {   // pinetreeWithAddress 변수 선언과 초기화
	name: "대나무한정식",
	star: 5,
	address: "동대문",
};

pinetree = pinetreeWithAddress;

pinetreeaddress를 추가한 객체를 할당한 것이 아닌,

address를 추가한 pinetreeWithAddresspinetree에 할당 할 경우에는 오류가 나지 않습니다.

이는 바로 타입 호환성 덕분입니다.

타입이 호환되기 위한 조건은 이렇게 할당하고자 하는 타입(pinetreeWithAddress)이

할당될 타입(pinetree)(Restaurant)의 구조와 내부 타입이 같아야 합니다.

이러한 개념을 구조적 타이핑(structural typing)이라 합니다.

구조적 타이핑이란 코드 구조 관점에서 타입이 서로 호환되는지의 여부를 판단하는 것으로

즉, 타입스크립트는 해당 인터페이스에서 정의한 프로퍼티나 메소드를 가지고 있다면

그 인터페이스를 구현한 것으로 인정한다는 것입니다.

pinetreeWithAddresspinetree(Restaurant)의 구조인

name, star를 포함하며 더 많은 속성을 가지고 있기 때문에

더 넓은 범위를 가지고 있다고 볼 수 있습니다.

기본적으로 타입 호환이라는 것은 오른쪽에 있는 타입이 더 많은 속성을 갖고 있거나 더 클 때 호환이 됩니다.

함수와 제네릭 그리고 클래스에서도 마찬가지로 타입호환이 됩니다.


8) 타입 별칭(Type Alias)

위에서 인터페이스를 설명하기전 언급한 타입 별칭에 대해 알아보겟습니다.

타입 별칭은 인터페이스와 거의 비슷한 기능을 합니다.

객체 또는 함수에 대한 타입 정의를 하는 일을 합니다. 또한 인터페이스를 참조 가능합니다.

 조금더 간단하게 말하자면, type 키워드로 기존의 타입에 이름을 붙여서 이용할 수 있게 하는 것입니다.

코드에서 함수 시그니처를 지정하고 함수를 선언하면, 코드가 너무 길어지고 복잡해보인다는 단점이 있습니다.

그래서 위와 같은 상황을 해결하고자 타입 부분은 타입 별칭으로 빼내고 간결하게 이용합니다.


함수 시그니처

함수 시그니처(Function Signature)는 함수의 타입을 의미합니다.

함수의 타입은 함수의 인자의 타입과 반환 값의 타입을 의미합니다. 시그니처는 다음과 같이 작성합니다.

(매개변수 : 타입, ...) => 반환값 타입

function add(x: number, y: number): number {
    return x+y;
}

위 함수의 타입은 인자 x: number, y: number 그리고 반환값 타입 number가 있습니다.

따라서 시그니처는 (number, number) => number가 됩니다.


//타입선언
type func1 = (param1: number, param2: number) => number;

const addFunc: func1 = (x: number, y: number): number => x + y;
type Developer = {
  name: string;
  skill: string;
};

타입별칭은 제네릭 역시 사용이 가능합니다

type typeGeneric<T> = {
  name: T;
};

타입별칭은 인터섹션(&)을 사용해 extends의 역활을 합니다.

type test1 = { name: string };

type test2 = test1 & { age: number };

const test3: test2 = { name: "d", age: 33 };
 

다만, extends처럼 상속을 받아 확장하는 개념은 아니고, 새로운 타입을 정의한 것입니다.

Type: 타입별칭Type Alias VS 인터페이스 Interface

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


const Kenny: Human = {
  name: '이병관',
  age: 26,
};


//


type Human = {
  name: string;
  age: number;
};


const Kenny: Human = {
  name: '이병관',
  age: 26,
};
////


type TypeFunction = {
  (x: number): number;
}


interface InterfaceFunction = {
  (x: number): number;
}


const typeFunction: TypeFunction = (x) => 0;
const interfaceFunction: InterfaceFunction = (x)

Type Alias와 Interface역시 타입과 함수타입을 정의 할수 있고, 아래에서 설명할

제네릭 역시 사용이 가능합니다.

type TypeGeneric<T> = {
  typer: T;
}


interface InterfaceGeneric<T> = {
  typer: T;
}

하지만 Type Alias 과 인터페이스는 몇가지 차이점이 존재합니다.

1) Union

유니온 타입(Union Type)이란 자바스크립트의 OR 연산자(||)와 같이

 'A' 이거나 'B'이다 라는 의미의 타입입니다.

type TypeAorB = "a" | "b"


interface InterfaceAorB {
  ...???//I cannot do that
}
....
//any를 사용하는 경우
function getAge(age: any) {
 age.toFixed(); // 에러 발생, age의 타입이 any로 추론되기 때문에 숫자 관련된 API를 작성할 때 코드가 자동 완성되지 않는다.
 return age;
}


// 유니온 타입을 사용하는 경우
function getAge(age: number | string) {
 if(typeof age ==number) {
  return age.toFixed(); // 정상 동작, age의 타입이 ‘number’로 추론되기 때문에 숫자 관련된 API를 쉽게 자동완성 할 수 있다.
 }
  
 if(typeof age == 'string') {
   return age;
 }
  
 return new TypeError('age must be number of string');

 any를 사용하는 경우 마치 자바스크립트로 작성하는 것처럼 동작을 하고

유니온 타입을 사용하면 타입스크립트의 이점을 살리면서 코딩을 할 수 있으나,

인터페이스에는 유니온이 존재하지 않아 복잡한 타입을 만들어내기 어렵습니다.

2) 프로토타입 체인 메소드

Type Alias를 이용하면 튜플과 배열 타입도 간결하게 표현할 수 있습니다.

interface를 사용하게 되면 유사하게 만들 수 있으나

튜플의 프로토타입 체인 상에 있는 메서드들을 사용할 수 없습니다.

한마디로 타입을 흉내낸 유사품입니다.

Type Alias을 사용하여 튜플을 만든다면 해당 프로토타입의 튜플의 메서드들을 사용할수 있으나

인터페이스를 사용하여 만들경우 해당 메소드에 접근 할 수 없습니다.

3) 강력한 선언 병합

하지만 인터페이스는 Type Alias에 존재하지 않는 강력한 기능을 가지고있습니다.

바로 '선언 병합(Declaration Merging)' 이라는 기능입니다.

선언 병합을 통해 인터페이스의 속성들을 계속 확장해 나갈수 잇습니다.

인터페이스를 다시 만들지 않고, 이전에 선언된 인터페이스에 계속 붙여 나갈수 잇는 점입니다.

//부트스트랩의 인터페이스 선언(@type/bootstrap 패키지)
declare global {
    interface JQuery {
        alert: Alert.jQueryInterface;
        button: Button.jQueryInterface;
        carousel: Carousel.jQueryInterface;
        collapse: Collapse.jQueryInterface;
        dropdown: Dropdown.jQueryInterface;
        tab: Tab.jQueryInterface;
        modal: Modal.jQueryInterface;
        offcanvas: Offcanvas.jQueryInterface;
        [Popover.NAME]: Popover.jQueryInterface;
        scrollspy: ScrollSpy.jQueryInterface;
        toast: Toast.jQueryInterface;
        [Tooltip.NAME]: Tooltip.jQueryInterface;
    }


    interface Element {
        addEventListener(
            type: Carousel.Events,
            listener: (this: Element, ev: Carousel.Event) => any,
            options?: boolean | AddEventListenerOptions,
        ): void;
    }
}

@type/bootstrap 패키지의 bootstrap 타입을 지정한 코드입니다.

해당 코드는 JQuery나 Element 쪽의 라이브러리의 타입 코드를 수정하지 않고

bootstrap 쪽에서 필요한 속성을 추가하고 있습니다.

이렇게 기존의 타입의 속성을 확장하기에 선언 병합을 보강(augment)라고도 부릅니다.

타입스크립트에서 전역 변수를 추가할 때에도 유용하게 선언 병합을 사용할 수 있습니다.

전역 변수이기에 Window 타입에 속성을 추가하면 되기 때문입니다.

declare:
declare 키워드는 컴파일러에게 해당 변수나 함수가 이미 존재한다는 것을 알리는 역할을 합니다
다른 영역의 코드에서 declare로 선언된 해당 변수나 함수를 참조할 수 있으며
declare로 선언된 부분은 javascript로 컴파일되지 않습니다.

즉 간단하게 정리하자면

Type Alias 와 인터페이스는 큰 부분에서는 차이가 나지 않지만,

복잡한 타입들을 다루기 위해선 Type Alias 가 유용하고,

추후에 속성들을 추가할것을 고려한다면 인터페이스가 적절하다는것을 알 수 잇습니다.

9) d.ts

d.ts 파일은 전역에서 사용할 수 타입 유형만 선언할 수 있는 파일입니다.

이 파일은 출력에서 JavaScript 코드를 생성하지 않는 TypeScript 코드만 포함할 수 있으며

이 때문에 d.ts 파일은 컴파일 이후 js코드로는 생성되지 않습니다.

declare const global = 'power';

이 파일에 작성되는 declare namespace 블록과 declare module 블록의 필드들에는 

export 키워드가 기본으로 붙기에 따로 명시하지 않아도 됩니다.

또한 선언 코드만 작성이 가능하고 일반 코드는 작성할 수 없습니다.

그렇기에 최상위에 존재하는 변수, 상수, 함수, 클래스, 네임스페이스의 선언 앞에는

반드시 declare 혹은 export가 붙어야 합니다.

9-1).d.ts 파일이 없는 javascript

typescript는 컴파일 과정을 거쳐 타입 검증을 하는데, 검증을 하려면 타입이 정의된 파일이 있어야 합니다.

typescript로 작성한 모듈이라면 문법 자체에 타입이 있으니 문제없지만

javascript로 작성된 모듈을 사용하려면 컴파일러가 타입 검증을 할 수 없게 됩니다.

모듈의 타입을 알려주기 위해 .d.ts 파일을 두어 타입을 선언해 줘야 하지만

.d.ts 은 타입스크립트의 컴파일 과정에 필요한 거라서

npm 같은 저장소에 공개된 모듈들의 필수사항이 아닙니다.

요즈음에는 @types/{module_name}의 타입 정의 파일도 배포되기 때문에

타입 전용 모듈도 받음으로써 컴파일 문제를 해결할 수 있으나, 만일 타입정의 파일을 만들지 않앗거나,

오타와 같은 실수로 잘못 정의되있을 경우 직접 타입 정의를 통해 가져올 수 있습니다

$ npm install uuid
 

import { v4 } from 'uuid'
console.log(uuidTest())
 //error TS7016: Could not find a declaration file for module 'uuid'.
//Try npm install @types/uuid if it exists or add a new declaration (.d.ts) file //containing declare module 'uuid';

흔한 uuid 모듈을 가져와 v4로 출력 하도록 작성했으나, 타입 정의 파일이 존재하지 않아 에러가 발생했습니다.

제시하는 해결방안은 @types/uuid를 사용하거나, 타입 정의 파일인 .d.ts를 만들라고 쓰여있습니다.

npm install @types/uuid

타입 정의 파일을 설치해보면

 ./node_modules/@types/uuid/index.d.ts 파일이 존재합니다.

export const v4: v4;로 v4를 export 하고 있고 v4 type을 따라가다 보면 아래 코드에 정의되어 있습니다.

export type v4String = (options?: V4Options) => string;
export type v4Buffer = <T extends OutputBuffer>(options: V4Options | null | undefined, buffer: T, offset?: number) => T;
export type v4 = v4Buffer & v4String;

또는 .d.ts 를 정의해 만들수 있습니다.

실제론 들어가는 파라미터도 존재하고 추가적인 타입과 제공하는 데이터도 있으나. 일단 간단하게만 설정하겠습니다.

declare module 'uuid' {
  const v4: () => string

  export { v4 }
}

이처럼 최소한으로 v4라는 함수 타입을 선언해주면 타입스크립트 컴파일에 성공하고,

javascript는 ./node\_modules/uuid를 수행하며 문제없이 동작합니다.

profile
뜨겁고 매콤하고 화끈하게

1개의 댓글

comment-user-thumbnail
2022년 6월 22일

잘읽었어요!!!!!

답글 달기