HEROPY
Tech
한눈에 보는 타입스크립트(updated)
{{ scrollPercentage }}%

한눈에 보는 타입스크립트(updated)

typescript

타입스크립트 개요

타입스크립트(TypeScript)는 Microsoft에서 개발하고 유지/관리하는 Apache 라이센스가 부여된 오픈 소스입니다.
일반 자바스크립트로 컴파일되는 자바스크립트 Superset(상위 호환)으로 2012년 10월에 처음 릴리스 되었습니다.

왜 타입스크립트인가?

C#과 Java 같은 체계적이고 정제된 언어들에서 사용하는 강한 타입 시스템은 높은 가독성과 코드 품질 등을 제공할 수 있고 런타임이 아닌 컴파일 환경에서 에러가 발생해 치명적인 오류들을 더욱더 쉽게 잡아낼 수 있습니다.

반면 자바스크립트는 타입 시스템이 없는 동적 프로그래밍 언어로, 자바스크립트 변수는 문자열, 숫자, 불린 등 여러 타입의 값을 가질 수 있습니다.
이를 약한 타입 언어라고 표현할 수 있으며 비교적 유연하게 개발할 수 있는 환경을 제공하는 한편 런타임 환경에서 쉽게 에러가 발생할 수 있는 단점을 가집니다.

그리고 타입스크립트는 이러한 자바스크립트에 강한 타입 시스템을 적용해 대부분의 에러를 컴파일 환경에서 코드를 입력하는 동안 체크할 수 있습니다.

타입스크립트 사용법

자바스크립트가 .js 확장자를 가진 파일로 작성되는 것과 같이 타입스크립트는 .ts 확장자를 가진 파일로 작성할 수 있고, 작성 후 타입스크립트 컴파일러를 통해 자바스크립트 파일로 컴파일하여 사용하게 됩니다.

$ tsc sample.ts
# compiled to `sample.js`

타입스크립트의 기능

  • 크로스 플랫폼 지원: 자바스크립트가 실행되는 모든 플랫폼에서 사용할 수 있습니다.
  • 객체 지향 언어: 클래스, 인터페이스, 모듈 등의 강력한 기능을 제공하며, 순수한 객체 지향 코드를 작성할 수 있습니다.
  • 정적 타입: 정적 타입을 사용하기 때문에 코드를 입력하는 동안에 오류를 체크할 수 있습니다.(단 에디터 혹은 플러그인의 도움의 필요)
  • DOM 제어: 자바스크립트와 같이 DOM을 제어해 요소를 추가하거나 삭제할 수 있습니다.
  • 최신 ECMAScript 기능 지원: ES6 이상의 최신 자바스크립트 문법을 손쉽게 지원할 수 있습니다.

개발환경

VSCode와 WebStorm

VSCode(Visual Studio Code)WebStorm은 타입스크립트 지원 기능이 내장되어 있기 때문에 별도의 설정 없이도 타입스크립트 파일을(.ts, tsconfig.json 등) 인식할 수 있고 코드 검사, 빠른 수정, 실행 및 디버깅 등의 다양한 기능을 바로 사용할 수 있습니다.
단, 컴파일러는 포함되어 있지 않기 때문에 별도로 설치해야 합니다.(E.g. npm install typescript)

VSCode Error

Visual Studio Code

WebStorm Error

WebStorm

컴파일러 설치

tsc 명령을 사용하기 위해 다음과 같이 타입스크립트를 전역 설치할 수 있습니다.
타입스크립트 파일을 경로로 지정하면 해당 파일을 컴파일합니다.

$ npm install -g typescript
$ tsc --version
$ tsc ./src/index.ts

혹은, 단일 프로젝트에서만 사용하길 희망하는 경우 일반 지역 설치 후 npx tsc 명령으로 실행할 수도 있습니다.

$ npm install typescript
$ npx tsc --version
$ npx tsc ./src/index.ts

컴파일러 옵션

타입스크립트 컴파일을 위한 다양한 옵션을 지정할 수 있습니다.

$ tsc ./src/index.ts --watch --strict true --target ES6 --lib ES2015,DOM --module CommonJS

혹은 아래와 같이 tsconfig.json 파일로 옵션을 관리할 수 있습니다.
"include""exclude" 옵션을 같이 추가해, 컴파일에 포함할 경로제외할 경로를 설정할 수 있습니다.

VScode와 WebStorm을 사용하는 경우, tsconfig.json 파일을 프로젝트 루트 경로에 생성하면 에디터에 의해 구성 옵션이 분석됩니다.

{
  "compilerOptions": {
    "strict": true,
    "target": "ES6",
    "lib": ["ES2015, DOM"],
    "module": "CommonJS"
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}
$ tsc --watch

TypeScript Playground

https://www.typescriptlang.org/play/index.html

타입스크립트 공식 페이지에서 제공하는 REPL로, 작성한 내용이 컴파일러 옵션에 따라 어떻게 자바스크립트로 변환되는지 바로 확인할 수 있습니다.

typescript playground

Repl.it

https://repl.it/languages/typescript

파일과 디렉터리로 관리되는 타입스크립트 프로젝트를 손쉽게 구성할 수 있습니다.
간단한 프로젝트로 타입스크립트를 테스트하기 좋습니다.

repl.io - typescript

Parcel

타입스크립트를 로컬 환경에서 빠르게 테스트하고 싶다면 Parcel 번들러가 좋은 선택입니다.
다음과 같이 간단하게 프로젝트를 구성합니다.

$ mkdir typescript-test
$ cd typescript-test
$ npm init -y
$ npm install typescript
$ npm install --save-dev parcel-bundler

프로젝트 구조

tsconfig.json 파일을 생성하고 원하는 옵션을 추가합니다.
다음은 예시입니다.

{
  "compilerOptions": {
    "strict": true
  },
  "exclude": [
    "node_modules"
  ]
}

main.ts 파일을 생성하고 원하는 타입스크립트 코드를 입력합니다.

function add(a: number, b: number) {
  return a + b;
}
const sum: number = add(1, 2);
console.log(sum);

index.html 파일을 생성하고 다음과 같이 .js가 아닌 .ts 파일을 연결합니다.
Parcel 번들러가 빌드시 자동으로 타입스크립트를 컴파일합니다.

<!doctype html>
<html>
<head>
  <meta charset="UTF-8">
  <title>TypeScript Test</title>
</head>
<body>
  <script src="main.ts"></script>
</body>
</html>

마지막으로 다음과 같이 진입 파일로 index.html를 지정하고 Parcel 번들러로 빌드합니다.

$ npx parcel index.html
# Server running at http://localhost:1234

타입 기본(Types)

타입 지정

타입스크립트는 일반 변수, 매개 변수(Parameter), 객체 속성(Property) 등에 : TYPE과 같은 형태로 타입을 지정할 수 있습니다.

function someFunc(a: TYPE_A, b: TYPE_B): TYPE_RETURN {
  return a + b;
}
let some: TYPE_SOME = someFunc(1, 2);

다음 예시를 보면,
add 함수의 매개 변수 abnumber 타입이어야 한다고 지정했고,
그렇게 실행된 함수의 반환 값은 숫자로 추론(Inference)되기 때문에 변수 sumnumber 타입이어야 한다고 지정했습니다.

function add(a: number, b: number) {
  return a + b;
}
const sum: number = add(1, 2);
console.log(sum); // 3

자바스크립트로 컴파일한 결과는 다음과 같습니다.

"use strict";
function add(a, b) {
  return a + b;
}
const sum = add(1, 2);
console.log(sum);

타입 에러

만약 다음과 같이 변수 sumnumber가 아닌 string 타입이어야 한다고 지정했다면, 컴파일조차 하지 않고 코드를 작성하는 시점에서 에러가 발생합니다.

function add(a: number, b: number) {
  return a + b;
}
const sum: string = add(1, 2);
console.log(sum);

Typescript error - TS2322

TS2322 Error

위 이미지에서 TS2322라는 에러 코드를 볼 수 있으며, 이를 검색하면 쉽게 에러 코드에 대한 정보를 얻을 수 있습니다.

Search Typescript error code

타입 선언

불린: Boolean

단순한 참(true)/거짓(false) 값을 나타냅니다.

let isBoolean: boolean;
let isDone: boolean = false;

숫자: Number

모든 부동 소수점 값을 사용할 수 있습니다.
ES6에 도입된 2진수 및 8진수 리터럴도 지원합니다.

let num: number;
let integer: number = 6;
let float: number = 3.14;
let hex: number = 0xf00d; // 61453
let binary: number = 0b1010; // 10
let octal: number = 0o744; // 484
let infinity: number = Infinity;
let nan: number = NaN;

문자열: String

문자열을 나타냅니다.
작은따옴표('), 큰따옴표(") 뿐만 아니라 ES6의 템플릿 문자열도 지원합니다.

let str: string;
let red: string = 'Red';
let green: string = "Green";
let myColor: string = `My color is ${red}.`;
let yourColor: string = 'Your color is' + green;

배열: Array

순차적으로 값을 가지는 일반 배열을 나타냅니다.
배열은 다음과 같이 두 가지 방법으로 타입을 선언할 수 있습니다.

// 문자열만 가지는 배열
let fruits: string[] = ['Apple', 'Banana', 'Mango'];
// Or
let fruits: Array<string> = ['Apple', 'Banana', 'Mango'];

// 숫자만 가지는 배열
let oneToSeven: number[] = [1, 2, 3, 4, 5, 6, 7];
// Or
let oneToSeven: Array<number> = [1, 2, 3, 4, 5, 6, 7];

Union 타입(다중 타입)의 ‘문자열과 숫자를 동시에 가지는 배열’도 선언할 수 있습니다.

let array: (string | number)[] = ['Apple', 1, 2, 'Banana', 'Mango', 3];
// Or
let array: Array<string | number> = ['Apple', 1, 2, 'Banana', 'Mango', 3];

배열이 가지는 항목의 값을 단언할 수 없다면 any를 사용할 수 있습니다.

let someArr: any[] = [0, 1, {}, [], 'str', false];

인터페이스(Interface)나 커스텀 타입(Type)을 사용할 수도 있습니다.

interface IUser {
  name: string,
  age: number,
  isVerified: boolean
}
let userArr: IUser[] = [
  {
    name: 'Neo',
    age: 85,
    isVerified: true
  },
  {
    name: 'Lewis',
    age: 52,
    isVerified: false
  },
  {
    name: 'Evan',
    age: 36,
    isVerified: true
  }
];

유용하진 않지만, 다음과 같이 특정한 값으로 타입을 대신해 작성할 수도 있습니다.

let array = 10[];
array = [10];
array.push(10);
array.push(11); // Error - TS2345

읽기 전용 배열을 생성할 수도 있습니다.
readonly 키워드나 ReadonlyArray 타입을 사용하면 됩니다.

let arrA: readonly number[] = [1, 2, 3, 4];
let arrB: ReadonlyArray<number> = [0, 9, 8, 7];

arrA[0] = 123; // Error - TS2542: Index signature in type 'readonly number[]' only permits reading.
arrA.push(123); // Error - TS2339: Property 'push' does not exist on type 'readonly number[]'.

arrB[0] = 123; // Error - TS2542: Index signature in type 'readonly number[]' only permits reading.
arrB.push(123); // Error - TS2339: Property 'push' does not exist on type 'readonly number[]'.

튜플: Tuple

Tuple 타입은 배열과 매우 유사합니다.
차이점이라면 정해진 타입의 고정된 길이(length) 배열을 표현합니다.

let tuple: [string, number];
tuple = ['a', 1];
tuple = ['a', 1, 2]; // Error - TS2322
tuple = [1, 'a']; // Error - TS2322

다음과 같이 데이터를 개별 변수로 지정하지 않고, 단일 Tuple 타입으로 지정해 사용할 수 있습니다.

// Variables
let userId: number = 1234;
let userName: string = 'HEROPY';
let isVerified: boolean = true;

// Tuple
let user: [number, string, boolean] = [1234, 'HEROPY', true];
console.log(user[0]); // 1234
console.log(user[1]); // 'HEROPY'
console.log(user[2]); // true

나아가 위 방식을 활용해 다음과 같은 Tuple 타입의 배열(2차원 배열)을 사용할 수 있습니다.

let users: [number, string, boolean][];
// Or
// let users: Array<[number, string, boolean]>;

users = [[1, 'Neo', true], [2, 'Evan', false], [3, 'Lewis', true]];

역시 값으로 타입을 대신할 수도 있습니다.

let tuple: [1, number];
tuple = [1, 2];
tuple = [1, 3];
tuple = [2, 3]; // Error - TS2322: Type '2' is not assignable to type '1'.

Tuple은 정해진 타입의 고정된 길이 배열을 표현하지만, 이는 할당(Assign)에 국한됩니다.
.push().splice() 등을 통해 값을 넣는 행위는 막을 수 없습니다.

let tuple: [string, number];
tuple = ['a', 1];
tuple = ['b', 2];
tuple.push(3);
console.log(tuple); // ['b', 2, 3];
tuple.push(true); // Error - TS2345: Argument of type 'true' is not assignable to parameter of type 'string | number'.

배열에서 사용한 것과 같이 readonly 키워드를 사용해 읽기 전용 튜플을 생성할 수도 있습니다.

let a: readonly [string, number] = ['Hello', 123];
a[0] = 'World'; // Error - TS2540: Cannot assign to '0' because it is a read-only property.

열거형: Enum

Enum은 숫자 혹은 문자열 값 집합에 이름(Member)을 부여할 수 있는 타입으로, 값의 종류가 일정한 범위로 정해져 있는 경우 유용합니다.

기본적으로 0부터 시작하며 값은 1씩 증가합니다.

enum Week {
  Sun,
  Mon,
  Tue,
  Wed,
  Thu,
  Fri,
  Sat
}

enum example

WebStorm을 통해 보여지는 enum

수동으로 값을 변경할 수 있으며, 값을 변경한 부분부터 다시 1씩 증가합니다.

enum example

Enum 타입의 재미있는 부분은 역방향 매핑(Reverse Mapping)을 지원한다는 것입니다.
이것은 열거된 멤버(Sun, Mon 같은)로 값에, 값으로 멤버에 접근할 수 있다는 것을 의미합니다.

Week를 콘솔로 출력합니다.

enum Week {
  // ...
}
console.log(Week);
console.log(Week.Sun); // 0
console.log(Week['Sun']); // 0
console.log(Week[0]); // 'Sun'

enum reverse mapping

역방향 매핑(Reverse Mapping)

추가로, Enum은 숫자 값 열거뿐만아니라 다음과 같이 문자열 값으로 초기화할 수 있습니다.
이 방법은 역방향 매핑(Reverse Mapping)을 지원하지 않으며 개별적으로 초기화해야 하는 단점이 있습니다.

enum Color {
  Red = 'red',
  Green = 'green',
  Blue = 'blue'
}
console.log(Color.Red); // red
console.log(Color['Green']); // green

모든 타입: Any

Any는 모든 타입을 의미합니다.
따라서 일반적인 자바스크립트 변수와 동일하게 어떤 타입의 값도 할당할 수 있습니다.
외부 자원을 활용해 개발할 때 불가피하게 타입을 단언할 수 없는 경우, 유용할 수 있습니다.

let any: any = 123;
any = 'Hello world';
any = {};
any = null;

다양한 값을 포함하는 배열을 나타낼 때 사용할 수도 있습니다.

const list: any[] = [1, true, 'Anything!'];

강한 타입 시스템의 장점을 유지하기 위해 Any 사용을 엄격하게 금지하려면, 컴파일 옵션 "noImplicitAny": true를 통해 Any 사용 시 에러를 발생시킬 수 있습니다.

객체: Object

기본적으로 typeof 연산자가 "object"로 반환하는 모든 타입을 나타냅니다.

컴파일러 옵션에서 엄격한 타입 검사(strict)를 true로 설정하면, null은 포함하지 않습니다.

let obj: object = {};
let arr: object = [];
let func: object = function () {};
let nullValue: object = null;
let date: object = new Date();
// ...

여러 타입의 상위 타입이기 때문에 그다지 유용하지 않습니다.
보다 정확하게 타입 지정을 하기 위해 다음과 같이 객체 속성(Properties)들에 대한 타입을 개별적으로 지정할 수 있습니다.

let userA: { name: string, age: number } = {
  name: 'HEROPY',
  age: 123
};

let userB: { name: string, age: number } = {
  name: 'HEROPY',
  age: false, // Error
  email: '[email protected]' // Error
};

반복적인 사용을 원하는 경우, interfacetype을 사용하는 것을 추천합니다.

interface IUser {
  name: string,
  age: number
}

let userA: IUser = {
  name: 'HEROPY',
  age: 123
};

let userB: IUser = {
  name: 'HEROPY',
  age: false, // Error
  email: '[email protected]' // Error
};

Null과 Undefined

기본적으로 Null과 Undefined는 모든 타입의 하위 타입으로, 다음과 같이 각 타입에 할당할 수 있습니다.
심지어 서로의 타입에도 할당 가능합니다.

let num: number = undefined;
let str: string = null;
let obj: { a: 1, b: false } = undefined;
let arr: any[] = null;
let und: undefined = null;
let nul: null = undefined;
let voi: void = null;
// ...

이는 컴파일 옵션 "strictNullChecks": true을 통해 엄격하게 Null과 Undefined 서로의 타입까지 더 이상 할당할 수 없게 합니다.
단, Void에는 Undefined를 할당할 수 있습니다.

let voi: void = undefined; // ok

Void

Void는 일반적으로 값을 반환하지 않는 함수에서 사용합니다.
: void 위치는 함수가 반환 타입을 명시하는 곳입니다.

function hello(msg: string): void {
  console.log(`Hello ${msg}`);
}

값을 반환하지 않는 함수는 실제로는 undefined를 반환합니다.

function hello(msg: string): void {
  console.log(`Hello ${msg}`);
}
const hi: void = hello('world'); // Hello world
console.log(hi); // undefined
// Error - TS2355: A function whose declared type is neither 'void' nor 'any' must return a value.
function hello(msg: string): undefined {
  console.log(`Hello ${msg}`);
}

Never

Never은 절대 발생하지 않을 값을 나타내며, 어떠한 타입도 적용할 수 없습니다.

function error(message: string): never {
  throw new Error(message);
}

보통 다음과 같이 빈 배열을 타입으로 잘못 선언한 경우, Never를 볼 수 있습니다.

const never: [] = [];
never.push(3); // Error - TS2345: Argument of type '3' is not assignable to parameter of type 'never'.

유니언(Union)

2개 이상의 타입을 허용하는 경우, 이를 유니언(Union)이라고 합니다.
|(vertical bar)를 통해 타입을 구분하며, ()는 선택 사항입니다.

let union: (string | number);
union = 'Hello type!';
union = 123;
union = false; // Error - TS2322: Type 'false' is not assignable to type 'string | number'.

함수(Function)

화살표 함수를 이용해 타입을 지정할 수 있습니다.
인수의 타입과 반환 값의 타입을 입력합니다.

// myFunc는 2개의 숫자 타입 인수를 가지고, 숫자 타입을 반환하는 함수.
let myFunc: (arg1: number, arg2: number) => number;
myFunc = function (x, y) {
  return x + y;
};

// 인수가 없고, 반환도 없는 경우.
let yourFunc: () => void;
yourFunc = function () {
  console.log('Hello world~');
};

타입 추론(Inference)

명시적으로 타입 선언이 되어있지 않은 경우, 타입스크립트는 타입을 추론해 제공합니다.
개념은 매우 단순합니다.

[추론]: 어떠한 판단을 근거로 삼아 다른 판단을 이끌어 냄.

let num = 12;
num = 'Hello type!'; // TS2322: Type '"Hello type!"' is not assignable to type 'number'.

변수 num을 초기화하면서 숫자 12를 할당해 Number 타입으로 추론되었고, 따라서 'Hello type!'이라는 String 타입의 값은 할당할 수 없기 때문에 에러가 발생합니다.

이렇게 타입스크립트가 타입을 추론하는 경우는 다음과 같습니다.

  • 초기화된 변수
  • 기본값이 설정된 매개 변수
  • 반환 값이 있는 함수
// 초기화된 변수 `num`
let num = 12;

// 기본값이 설정된 매개 변수 `b`
function add(a: number, b: number = 2): number {
  // 반환 값(`a + b`)이 있는 함수
  return a + b;
}

타입 추론이 엄격하지 않은 타입 선언을 의미하는 것은 아닙니다.
따라서 이를 활용해 모든 곳에 타입을 명시할 필요는 없으며, 많은 경우 더 좋은 코드 가독성을 제공할 수 있습니다.

타입 단언(Assertions)

타입스크립트가 타입 추론을 통해 판단할 수 있는 타입의 범주를 넘는 경우, 더 이상 추론하지 않도록 지시할 수 있습니다.
이를 ‘타입 단언’이라고 하며, 이는 프로그래머가 타입스크립트보다 타입에 대해 더 잘 이해하고 있는 상황을 의미합니다.

[단언]: 주저하지 아니하고 딱 잘라 말함.

다음 예제를 살펴봅시다.

함수의 매개 변수 val은 Union 타입으로 문자열(String)이거나 숫자(Number)일 수 있습니다.
그리고 매개 변수 isNumber는 불린(Boolean)이며, 이름을 통해 숫자 여부를 확인하는 값이라는 것을 (우리는) 추론할 수 있습니다.
따라서 우리는 isNumbertrue일 경우 val은 숫자일 것이고, 이에 toFixed를 사용할 수 있음을 확실히 알 수 있습니다.
하지만 타입스크립트는 ‘isNumber’라는 이름만으로 위 내용을 추론할 수 없기 때문에 “val이 문자열인 경우 toFixed를 사용할 수 없다”고 (컴파일 단계에서) 다음과 같은 에러를 반환합니다.

function someFunc(val: string | number, isNumber: boolean) {
  // some logics
  if (isNumber) {
    val.toFixed(2); // Error - TS2339: ... Property 'toFixed' does not exist on type 'string'.
  }
}

따라서 우리는 isNumbertrue일 때 val이 숫자임을 다음과 같이 (2가지 방식으로) 단언할 수 있습니다.
두 번째 방식(<number>val)은 JSX를 사용하는 경우 특정 구문 파싱에서 문제가 발생할 수 있으며, 결과적으로 .tsx 파일에서는 전혀 사용할 수 없습니다.

function someFunc(val: string | number, isNumber: boolean) {
  // some logics
  if (isNumber) {
    (val as number).toFixed(2);
    // Or
    // (<number>val).toFixed(2);
  }
}

타입 단언은 마치 프로그래머가 타입스크립트에게 “나는 알고 있으니까 나를 믿어!”라고 알려주는 것과 같습니다.

Non-null 단언 연산자

!를 사용하는 Non-null 단언 연산자(Non-null assertion operator)를 통해 피연산자가 Nullish(null이나 undefined) 값이 아님을 단언할 수 있는데, 변수나 속성에서 간단하게 사용할 수 있기 때문에 유용합니다.

다음 예제 중 fnA 함수를 살펴보면, 매개 변수 x는 함수 내에서 toFixed를 사용하는 숫자 타입으로 처리되지만 null이나 undefined일 수 있기 때문에 에러가 발생합니다.
이를 타입 단언이나 if 조건문으로 해결할 수도 있지만, 마지막 함수와 같이 !를 사용하는 Non-null 단언 연산자를 이용해 간단하게 정리할 수 있습니다.

// Error - TS2533: Object is possibly 'null' or 'undefined'.
function fnA(x: number | null | undefined) {
  return x.toFixed(2);
}

// if statement
function fnD(x: number | null | undefined) {
  if (x) {
    return x.toFixed(2);
  }
}

// Type assertion
function fnB(x: number | null | undefined) {
  return (x as number).toFixed(2);
}
function fnC(x: number | null | undefined) {
  return (<number>x).toFixed(2);
}

// Non-null assertion operator
function fnE(x: number | null | undefined) {
  return x!.toFixed(2);
}

특히 컴파일 환경에서 체크하기 어려운 DOM 사용에서 유용합니다.
물론 일반적인 타입 단언을 사용할 수도 있습니다.

// Error - TS2531: Object is possibly 'null'.
document.querySelector('.menu-item').innerHTML;

// Type assertion
(document.querySelector('.menu-item') as HTMLDivElement).innerHTML;
(<HTMLDivElement>document.querySelector('.menu-item')).innerHTML;

// Non-null assertion operator
document.querySelector('.menu-item')!.innerHTML;

타입 가드(Guards)

다음 예제와 같이 val의 타입을 매번 보장하기 위해 타입 단언을 여러 번 사용하게 되는 경우가 있습니다.

function someFunc(val: string | number, isNumber: boolean) {
  if (isNumber) {
    (val as number).toFixed(2);
    isNaN(val as number);
  } else {
    (val as string).split('');
    (val as string).toUpperCase();
    (val as string).length;
  }
}

이 경우 타입 가드를 제공하면 타입스크립트가 추론 가능한 특정 범위(scope)에서 타입을 보장할 수 있습니다.
타입 가드는 NAME is TYPE 형태의 타입 술부(Predicate)를 반환 타입으로 명시한 함수입니다.
다음 예제에서 타입 술부는 val is number입니다.
타입 단언이 없어지니 훨씬 깔끔합니다.

[술부]: 주어의 상태, 성질 따위를 서술하는 말.

// 타입 가드
function isNumber(val: string | number): val is number {
  return typeof val === 'number';
}

function someFunc(val: string | number) {
  if (isNumber(val)) {
    val.toFixed(2);
    isNaN(val);
  } else {
    val.split('');
    val.toUpperCase();
    val.length;
  }
}

위 방식뿐만 아니라 제공 가능한 2가지 타입 가드가 더 있습니다.
typeofinstanceof 연산자를 직접 사용하는 타입 가드입니다.
비교적 단순한 로직에서 추천되는 방식입니다.

typeof 연산자는 number, string, boolean, 그리고 symbol만 타입 가드로 인식할 수 있습니다.

// 기존 예제와 같이 `isNumber`를 제공(추상화)하지 않아도 `typeof` 연산자를 직접 사용하면 타입 가드로 동작합니다.
function someFunc(val: string | number) {
  if (typeof val === 'number') {
    val.toFixed(2);
    isNaN(val);
  } else {
    val.split('');
    val.toUpperCase();
    val.length;
  }
}

// 역시 별도의 추상화 없이 `instanceof` 연산자를 사용해 타입 가드를 제공합니다.
class Cat {
  meow() {}
}
class Dog {
  woof() {}
}
function sounds(ani: Cat | Dog) {
  if (ani instanceof Cat) {
    ani.meow();
  } else {
    ani.woof();
  }
}

인터페이스(interface)

인터페이스(Interface)는 타입스크립트 객체(Objects)를 정의하는 일종의 규칙이며 구조입니다.
속성(Property)과 메소드(Method) 등의 타입을 정의할 수 있습니다.
다음과 같이 interface 키워드와 함께 사용합니다.

‘IUser’에서 ‘I’는 Interface를 의미하는 별칭으로 사용했습니다.

interface IUser {
  name: string,
  age: number,
  isAdult: boolean
}

let user1: IUser = {
  name: 'Neo',
  age: 123,
  isAdult: true
};

// Error - TS2741: Property 'isAdult' is missing in type '{ name: string; age: number; }' but required in type 'IUser'.
let user2: IUser = {
  name: 'Evan',
  age: 456
};

:(colon), ,(comma) 혹은 기호를 사용하지 않을 수 있습니다.

interface IUser {
  name: string,
  age: number
}
// Or
interface IUser {
  name: string;
  age: number;
}
// Or
interface IUser {
  name: string
  age: number
}

다음과 같이 속성에 ?를 사용하면 선택적 속성으로 정의할 수 있습니다.
선택적 속성(Optional properties)에 대해선 Optional 파트에서 따로 설명하지만, 간단하게 표현하면 ‘필수가 아닌 속성으로 정의’하는 방법을 말합니다.

interface IUser {
  name: string,
  age: number,
  isAdult?: boolean // Optional property
}

// `isAdult`를 초기화하지 않아도 에러가 발생하지 않습니다.
let user: IUser = {
  name: 'Neo',
  age: 123
};

읽기 전용 속성(Readonly properties)

readonly 키워드를 사용하면 초기화된 값을 유지해야 하는 읽기 전용 속성을 정의할 수 있습니다.

interface IUser {
  readonly name: string,
  age: number
}

// 초기화
let user: IUser = {
  name: 'Neo',
  age: 36
};

user.age = 85; // Ok
user.name = 'Evan'; // Error - TS2540: Cannot assign to 'name' because it is a read-only property.

만약 모든 속성이 readonly일 경우, 유틸리티(Utility)나 단언(Assertion) 타입을 활용할 수 있습니다.

// All readonly properties
interface IUser {
  readonly name: string,
  readonly age: number
}
let user: IUser = {
  name: 'Neo',
  age: 36
};
user.age = 85; // Error
user.name = 'Evan'; // Error


// Readonly Utility
interface IUser {
  name: string,
  age: number
}
let user: Readonly<IUser> = {
  name: 'Neo',
  age: 36
};
user.age = 85; // Error
user.name = 'Evan'; // Error


// Type assertion
let user = {
  name: 'Neo',
  age: 36
} as const;
user.age = 85; // Error
user.name = 'Evan'; // Error

인덱싱 가능 타입(Indexable types)과 인덱스 시그니처(Index signature)

우리는 인터페이스를 통해 특정 속성(메소드 등)의 타입을 정의할 순 있지만, 수많은 속성을 가지거나 단언할 수 없는 임의의 속성이 포함되는 구조에서는 기존의 방식만으론 한계가 있습니다. 이런 상황에서 유용한 인덱스 시그니처(Index signature)에 대해서 살펴봅시다.

arr[2]와 같이 ‘숫자’로 인덱싱하거나 obj['name']과 같이 ‘문자’로 인덱싱하는, 인덱싱 가능 타입(Indexable types)들이 있습니다.
이런 인덱싱 가능 타입들을 정의하는 인터페이스는 인덱스 시그니처(Index signature)라는 것을 가질 수 있습니다.
인덱스 시그니처는 다음 구조와 같이, 인덱싱에 사용할 인덱서(Indexer)의 이름과 타입 그리고 인덱싱 결과의 반환 값을 지정합니다.
인덱서의 타입은 stringnumber만 지정할 수 있습니다.

interface INAME {
  [INDEXER_NAME: INDEXER_TYPE]: RETURN_TYPE // Index signature
}

배열(객체)에서 위치를 가리키는 숫자(문자)를 인덱스(index)라고 하며, 각 배열 요소(객체 속성)에 접근하기 위하여 인덱스를 사용하는 것을 인덱싱(indexing)이라고 합니다.(배열을 구성하는 각각의 값은 배열 요소(element)라고 합니다)

이해를 돕기 위해 다음 예제를 살펴보면,
인터페이스 IItem은 인덱스 시그니처를 가지고 있으며, 그 IItem을 타입(인터페이스)으로 하는 item이 있고, 그 itemitem[0]이나 item[1]과 같이 숫자로 인덱싱할 때 반환되는 값은 'a'b' 같은 문자여야 합니다.
itemitem['0']과 같이 문자로 인덱싱하는 경우 에러가 발생합니다.

interface IItem {
  [itemIndex: number]: string // Index signature
}
let item: IItem = ['a', 'b', 'c']; // Indexable type
console.log(item[0]); // 'a' is string.
console.log(item[1]); // 'b' is string.
console.log(item['0']); // Error - TS7015: Element implicitly has an 'any' type because index expression is not of type 'number'.

참고로 인덱싱 결과의 반환 타입으로 유니온을 사용하면 다음과 같이 활용할 수 있습니다.

interface IItem {
  [itemIndex: number]: string | boolean | number[]
}
let item: IItem = ['Hello', false, [1, 2, 3]];
console.log(item[0]); // Hello
console.log(item[1]); // false
console.log(item[2]); // [1, 2, 3]

이번에는 문자로 인덱싱하는 예제를 살펴봅시다.
인터페이스 IUser는 인덱스 시그니처를 가지고 있으며, 그 IUser를 타입(인터페이스)로 하는 user가 있고, 그 useruser['name'], user['email'] 또는 user['isValidate']와 같이 문자로 인덱싱할 때 반환되는 값은 'Neo''[email protected]' 같은 문자 혹은 true 같은 불린이어야 합니다.
또한 user[0]과 같은 숫자로 인덱싱하는 경우나 user['0']과 같이 문자로 인덱싱하는 경우 모두 인덱싱 전에 숫자가 문자열로 변환되기 때문에 다음과 같이 값을 반환할 수 있습니다.

interface IUser {
  [userProp: string]: string | boolean | any
}
let user: IUser = {
  name: 'Neo',
  email: '[email protected]',
  isValidate: true,
  0: false
};
console.log(user['name']); // 'Neo' is string.
console.log(user['email']); // '[email protected]' is string.
console.log(user['isValidate']); // true is boolean.
console.log(user[0]); // false is boolean
console.log(user[1]); // undefined
console.log(user['0']); // false is boolean

인덱스 시그니처를 사용하면 다음 예제와 같이 인터페이스에 정의되지 않은 속성들을 사용할 때 유용합니다.
단, 해당 속성이 인덱스 시그니처에 정의된 반환 값을 가져야 함에 주의해야 합니다.
다음 예제에서 isAdult 속성은 정의된 string이나 number 타입을 반환하지 않지 않기 때문에 에러가 발생합니다.

interface IUser {
  [userProp: string]: string | number
  name: string,
  age: number
}
let user: IUser = {
  name: 'Neo',
  age: 123,
  email: '[email protected]',
  isAdult: true // Error - TS2322: Type 'true' is not assignable to type 'string | number'.
};
console.log(user['name']); // 'Neo'
console.log(user['age']); // 123
console.log(user['email']); // [email protected]

제네릭(Generic)

Generic은 재사용을 목적으로 함수나 클래스의 선언 시점이 아닌, 사용 시점에 타입을 선언할 수 있는 방법을 제공합니다.

타입을 인수로 받아서 사용한다고 이해하면 쉽습니다.

다음 예제는 toArray 함수가 인수로 받은 값을 배열로 반환하도록 작성되었습니다.
매개 변수가 Number 타입만 허용하기 때문에 String 타입을 인수로 하는 함수 호출에서 에러가 발생합니다.

function toArray(a: number, b: number): number[] {
  return [a, b];
}
toArray(1, 2);
toArray('1', '2'); // Error - TS2345: Argument of type '"1"' is not assignable to parameter of type 'number'.

조금 더 범용적으로 만들기 위해 Union 방식을 사용했습니다.
이제 String 타입을 인수로 받을 수 있지만, 가독성이 떨어지고 새로운 문제도 발생했습니다.
세 번째 호출을 보면 의도치 않게 Number와 String 타입을 동시에 받을 수 있게 되었습니다.

function toArray(a: number | string, b: number | string): (number | string)[] {
  return [a, b];
}
toArray(1, 2); // Only Number
toArray('1', '2'); // Only String
toArray(1, '2'); // Number & String

이번에는 Generic을 사용합니다.
함수 이름 우측에 <T>를 작성해 시작합니다.
T는 타입 변수(Type variable)로 사용자가 제공한 타입으로 변환될 식별자입니다.
이제 세 번째 호출은 의도적으로 Number와 String 타입을 동시에 받을 수 있습니다.(혹은 Union을 사용하지 않으면 에러가 발생합니다)

타입 변수는 매개 변수처럼 원하는 이름으로 지정할 수 있습니다.

function toArray<T>(a: T, b: T): T[] {
  return [a, b];
}

toArray<number>(1, 2);
toArray<string>('1', '2');
toArray<string | number>(1, '2');
toArray<number>(1, '2'); // Error

타입 추론을 활용해, 사용 시점에 타입을 제공하지 않을 수 있습니다.

function toArray<T>(a: T, b: T): T[] {
  return [a, b];
}

toArray(1, 2);
toArray('1', '2');
toArray(1, '2'); // Error

클래스

클래스의 생성자 메소드(constructor)와 일반 메소드(Methods) 멤버(Class member)와는 다르게, 속성(Properties)은 name: string;와 같이 클래스 바디(Class body)에 별도로 타입을 선언합니다.

클래스 바디(Class body)는 중괄호 {}로 묶여 있는 영역을 의미합니다.

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}
class Cat extends Animal {
  getName(): string {
    return `Cat name is ${this.name}.`;
  }
}
let cat: Cat;
cat = new Cat('Lucy');
console.log(cat.getName()); // Cat name is Lucy.

클래스 수식어(Modifiers)

타입스크립트와 관련된 클래스 수식어들을 살펴봅시다.

클래스 멤버(속성, 메소드)에서 사용할 수 있는 접근 제어자(Access Modifiers)들이 있습니다.
각 접근 제어자들의 차이점을 이해해 봅시다.

접근 제어자(Access Modifiers)는 클래스, 메서드 및 기타 멤버의 접근 가능성을 설정하는 객체 지향 언어의 키워드입니다.

접근 제어자의미범위
public어디서나 자유롭게 접근 가능(생략 가능)속성, 메소드
protected나와 파생된 후손 클래스 내에서 접근 가능속성, 메소드
private내 클래스에서만 접근 가능속성, 메소드

다음 수식어들은 위 접근 제어자와 함께 사용할 수 있습니다.
static의 경우 타입스크립트에서는 정적 메소드뿐만 아니라 정적 속성도 사용할 수 있습니다.

수식어의미범위
static정적으로 사용속성, 일반 메소드
readonly읽기 전용으로 사용속성

그럼 우선, 각 접근 제어자들의 차이점에 대해서 살펴봅시다.

다음 예제의 Animal 클래스의 name 속성은 public이기 때문에 파생된 자식 클래스(Cat)에서 this.name으로 참조하거나 인스턴스에서 cat.name으로 접근하는데 아무런 문제가 없습니다.
(어디서나 자유롭게 접근 가능(생략 가능))

class Animal {
  // public 수식어 사용(생략 가능)
  public name: string;
  constructor(name: string) {
    this.name = name;
  }
}
class Cat extends Animal {
  getName(): string {
    return `Cat name is ${this.name}.`;
  }
}
let cat = new Cat('Lucy');
console.log(cat.getName()); // Cat name is Lucy.

cat.name = 'Tiger';
console.log(cat.getName()); // Cat name is Tiger.

다음 예제의 Animal 클래스의 name 속성은 protected이기 때문에 파생된 자식 클래스(Cat)에서 this.name으로 참조할 수는 있지만, 인스턴스에서 cat.name으로는 접근할 수 없습니다.
(나와 파생된 후손 클래스 내에서 접근 가능)

class Animal {
  // protected 수식어 사용
  protected name: string;
  constructor(name: string) {
    this.name = name;
  }
}
class Cat extends Animal {
  getName(): string {
    return `Cat name is ${this.name}.`;
  }
}
let cat = new Cat('Lucy');
console.log(cat.getName()); // Cat name is Lucy.
console.log(cat.name); // Error - TS2445: Property 'name' is protected and only accessible within class 'Animal' and its subclasses.

cat.name = 'Tiger'; // Error - TS2445: Property 'name' is protected and only accessible within class 'Animal' and its subclasses.
console.log(cat.getName());

다음 예제의 Animal 클래스의 name 속성은 private이기 때문에 파생된 자식 클래스(Cat)에서 this.name으로 참조할 수 없고, 인스턴스에서도 cat.name으로 접근할 수도 없습니다.
(내 클래스에서만 접근 가능)

class Animal {
  // private 수식어 사용
  private name: string;
  constructor(name: string) {
    this.name = name;
  }
}
class Cat extends Animal {
  getName(): string {
    return `Cat name is ${this.name}.`; // Error - TS2341: Property 'name' is private and only accessible within class 'Animal'
  }
}
let cat = new Cat('Lucy');
console.log(cat.getName());
console.log(cat.name); // Error - TS2341: Property 'name' is private and only accessible within class 'Animal'.

cat.name = 'Tiger'; // Error - TS2341: Property 'name' is private and only accessible within class 'Animal'.
console.log(cat.getName());

다음은 생성자 메소드(constructor)에 protected를 사용했기 때문에 인스턴스 생성에서 에러가 발생하는 예제입니다.

class Animal {
  name: string;
  protected constructor(name: string) {
    this.name = name;
  }
}
const cat = new Animal('Dog'); // Error - TS2674: Constructor of class 'Animal' is protected and only accessible within the class declaration.

그리고 흥미로운 부분은 생성자 메소드에서 인수 타입 선언과 동시에 접근 제어자를 사용하면 바로 속성 멤버로 정의할 수 있습니다.
단, public은 생략할 수 없습니다.

class Cat {
  constructor(public name: string, protected age: number) {}
  getName() {
    return this.name;
  }
  getAge() {
    return this.age;
  }
}

const cat = new Cat('Neo', 2);
console.log(cat.getName()); // Neo
console.log(cat.getAge()); // 2

이번엔 staticreadonly에 대해서 살펴봅시다.

ES6에서는 static으로 정적 메소드만 생성할 수 있었는데, 타입스크립트에서는 정적 속성도 생성할 수 있습니다.
정적 속성은 클래스 바디에서 속성의 타입 선언과 같이 사용하며, 정적 메소드와 다르게 클래스 바디에서 값을 초기화할 수 없기 때문에 constructor 혹은 메소드에서 초기화가 필요합니다.

class Cat {
  static legs: number;
  constructor() {
    Cat.legs = 4; // Init static property.
  }
}
console.log(Cat.legs); // undefined
new Cat();
console.log(Cat.legs); // 4

class Dog {
  // Init static method.
  static getLegs() {
    return 4;
  }
}
console.log(Dog.getLegs()); // 4

readonly을 사용하면 해당 속성은 ‘읽기 전용’입니다.

class Animal {
  readonly name: string;
  constructor(n: string) {
    this.name = n;
  }
}
let dog = new Animal('Charlie');
console.log(dog.name); // Charlie
dog.name = 'Tiger'; // Error - TS2540: Cannot assign to 'name' because it is a read-only property.

그리고 staticreadonly는 접근 제어자와 같이 사용할 수 있습니다.
접근 제어자를 먼저 작성해야 합니다.

class Cat {
  public readonly name: string;
  protected static eyes: number;
  constructor(n: string) {
    this.name = n;
    Cat.eyes = 2;
  }
  private static getLegs() {
    return 4;
  }
}

추상(Abstract) 클래스

추상(Abstract) 클래스는 다른 클래스가 파생될 수 있는 기본 클래스로, 인터페이스와 굉장히 유사합니다.
abstract는 클래스뿐만 아니라 속성과 메소드에도 사용할 수 있습니다.
추상 클래스는 직접 인스턴스를 생성할 수 없기 때문에 파생된 후손 클래스에서 인스턴스를 생성해야 합니다.

// Abstract Class
abstract class Animal {
  abstract name: string; // 파생된 클래스에서 구현해야 합니다.
  abstract getName(): string; // 파생된 클래스에서 구현해야 합니다.
}
class Cat extends Animal {
  constructor(public name: string) {
    super();
  }
  getName() {
    return this.name;
  }
}
new Animal(); // Error - TS2511: Cannot create an instance of an abstract class.
const cat = new Cat('Lucy');
console.log(cat.getName()); // Lucy

// Interface
interface IAnimal {
  name: string;
  getName(): string;
}
class Dog implements IAnimal {
  constructor(public name: string) {}
  getName() {
    return this.name;
  }
}

추상 클래스가 인터페이스와 다른 점은 속성이나 메소드 멤버에 대한 세부 구현이 가능하다는 점입니다.

abstract class Animal {
  abstract name: string;
  abstract getName(): string;
  // Abstract class constructor can be made protected.
  protected constructor(public legs: string) {}
  getLegs() {
    return this.legs
  }
}

Optional

? 키워드를 사용하는 여러 선택적(Optional) 개념에 대해서 살펴봅시다.

매개 변수(Parameters)

우선, 타입을 선언할 때 선택적 매개 변수(Optional Parameter)를 지정할 수 있습니다.
다음 예제를 보면 ? 키워드를 사용해 y를 선택적 매개 변수로 지정했습니다.
따라서 y가 받을 인수가 없어도 에러가 발생하지 않습니다.

function add(x: number, y?: number): number {
  return x + (y || 0);
}
const sum = add(2);
console.log(sum);

위 예제는 정확히 다음 예제와 같습니다.
즉, ? 키워드 사용은 | undefined를 추가하는 것과 같습니다.

function add(x: number, y: number | undefined): number {
  return x + (y || 0);
}
const sum = add(2, undefined);
console.log(sum);

속성과 매소드(Properties and Methods)

? 키워드를 속성(Properties)과 메소드(Methods) 타입 선언에도 사용할 수 있습니다.
다음은 인터페이스 파트에서 살펴봤던 예제입니다.
isAdult를 선택적 속성으로 선언하면서 더 이상 에러가 발생하지 않습니다.

interface IUser {
  name: string,
  age: number,
  isAdult?: boolean
}

let user1: IUser = {
  name: 'Neo',
  age: 123,
  isAdult: true
};

let user2: IUser = {
  name: 'Evan',
  age: 456
};

Type이나 Class에서도 사용할 수 있습니다.

interface IUser {
  name: string,
  age: number,
  isAdult?: boolean,
  validate?(): boolean
}
type TUser = {
  name: string,
  age: number,
  isAdult?: boolean,
  validate?(): boolean
}
abstract class CUser {
  abstract name: string;
  abstract age: number;
  abstract isAdult?: boolean;
  abstract validate?(): boolean;
}

체이닝(Chaining)

다음 예제는 str 속성이 undefined일 경우 toString 메소드를 사용할 수 없기 때문에 에러가 발생합니다.
str 속성이 문자열이라는 것을 단언하면 문제를 해결할 수 있지만, 더 간단하게 선택적 체이닝(Optional Chaining) 연산자 ?.를 사용할 수 있습니다.

자세한 사용법은 MDN 문서를 참고하세요.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Optional_chaining

obj?.prop;
obj?.[expr];
arr?.[index];
func?.(args);
// Error - TS2532: Object is possibly 'undefined'.
function toString(str: string | undefined) {
  return str.toString();
}

// Type Assertion
function toString(str: string | undefined) {
  return (str as string).toString();
}

// Optional Chaining
function toString(str: string | undefined) {
  return str?.toString();
}

특히 && 연산자를 사용해 각 속성을 Nullish 체크(null이나 undefined를 확인)하는 부분에서 유용합니다.

// Before
if (foo && foo.bar && foo.bar.baz) {}

// After-ish
if (foo?.bar?.baz) {}

Nullish 병합 연산자

일반적으로 논리 연산자 ||를 사용해 Falsy 체크(0, "", NaN, null, undefined를 확인)하는 경우가 많습니다.
여기서 0이나 "" 값을 유효 값으로 사용하는 경우 원치 않는 결과가 발생할 수 있는데, 이럴 때 유용한 Nullish 병합(Nullish Coalescing) 연산자 ??를 타입스크립트에서 사용할 수 있습니다.

자세한 사용법은 MDN 문서를 참고하세요.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator

const foo = null ?? 'Hello nullish.';
console.log(foo); // Hello nullish.

const bar = false ?? true;
console.log(bar); // false

const baz = 0 ?? 12;
console.log(baz); // 0

함수

모듈

참고 자료(References)

https://www.typescriptlang.org/docs/home.html
https://www.tutorialsteacher.com/typescript
https://hyunseob.github.io/2017/12/12/typescript-type-inteference-and-type-assertion/
https://jsdev.kr/t/typescript-interface/3168

공지 이미지
{{ title }}
{{ eventPlace }}
{{ eventDate }} ({{ eventDay }}) {{ eventTime }}
오늘 하루 그만 보기