Angular essentials - typeScript

  • SUNGMIN SHIN
  • 109 Minutes
  • 2018년 7월 23일

Angular essentials

(https://book.naver.com/bookdb/book_detail.nhn?bid=13761643)


04. TypeScript


4.1 TypeScript 개요

HTML5가 등장하기 이전까지 웹 애플리케이션은 플래시, 실버라이트, 액티브엑스와 같은 플러그인에 의존하여 인터랙티브한 웹페이지를 구축해왔으나 HTML5가 등장함으로써 플러그 인에 의존하던 구축방식은 자바스크립트로 대체되었다.
또한 AJAX의 활성화로 데스크톱 애플리케이션과 유사한 사용자 경험을 제공할 수 있는 SPA(Single Page Application)가 대세가 되었고 과거 서버 측이 담당하던 업무의 많은 부분이 클라이언트 측으로 이동하게 되었고 자바스크립트는 웹의 어셈블리언어로 불릴만킄 중요한 언어로 그 위상이 높아지게 되었음

모든 프로그래밍 언어에 장, 단점이 있듯 자바스크립트도 언어가 잘 정제되기 이전에 서둘러 출시된 문제와 과거 웹페이지의 보조적인 기능을 수행하기 위해 한정적인 용도로 만들어진 태생적 한계로 좋은 점도 나쁜 점도 많은 것이 사실이며 C나 java와 같은 C-Family 언어와는 구별되는 아래와 같은 특성이 있다.

이와 같은 특성은 코드가 복잡해질 수 있고 디버그와 테스트 공수가 증가하는 등의 문제를 일으 킬 수 있어 특히 규모가 큰 프로젝트에서 주의하여야 한다.
위와 같은 자바스크립트의 태생적 문제는 극복하기 위해 CoffeeScript, Dart, Haxe와 같은 AltJS(자바스크립트 대체언어)가 등장하였다.

TypeScript 또한 자바스크립트 대체 언어의 하나로 JavaScript(ES5)의 상위 집합(superset)으로 2012년 MS에서 발표, 정적 타이핑을 지원하며 ES6의 class, module과 ES7의 데코레이터 등을 지원한다.

TypeScript는 ES5의 상위 집합이므로 기존의 자바스크립트(ES5) 문법을 그대로 사용할 수 있으며 ES6의 새로운 기능을 사용하기 위해 바벨(babel)과 같은 별도의 트랜스파일러를 사용하지 않아도 ES6의 새로운 기능을 기존의 자바스크립트 엔진에서 실행할 수 있다. (TypeScript도 컴파일 과정을 거쳐야 하므로 실제 작업 과정 자체는 비슷하다고 볼 수 있음)

TypeScript는 이후 ECMAScript의 업그레이드에 따른 새로운 기능을 계속 추가할 예정이어서 매년 업그레이드 될 ECMAScript의 표준을 따라갈 수 있는 좋은 수단이 될 것


4.2 TypeScript의 장점

정적 타입

1
2
3
4
5
6
7
8
9
10
11
12
// javascript

/*
2개의 매개변수 a,b를 받아 더한 값을 반환하는 함수
개발자는 의도는 매개변수로 number로 전달받는 것이였으나 코드 어디에도 표시되지 않으며 자바스크립트 문법상의 어떤 문제도 없음
*/
function sum(a, b) {
return a + b;
}

// 아래와 같이 사용하면 의도되지 않은 결과를 반환
sum('x', 'y');

위 코드는 개발자의 의도대로 실행되진 않지만 문법상에 어떠한 문제도 없으며 에러 없이 정상적으로 실행하게 된다.
이는 변수나 반환 값의 타입을 사전에 지정하지 않는 자바스크립트의 동적 타이핑(Dynamic Typing)에 의한 것

1
2
3
4
5
6
7
8
9
// typescript

// 매개변수 및 반환 값의 type을 설정
function sum(a: number, b: number): number {
return a + b
}

// 아래와 같이 설정한 타입과 다른 값을 사용하면 컴파일 단계에서 오류 발생
sum('x', 'y');

위와 같이 TypeScript는 정적 타입(static type)을 지원하므로 컴파일 단계에서 오류를 포착할 수 있으며 명시적인 정적 타입의 지정으로 개발자의 의도를 명확하게 코드로 기술하여 코드의 가독성을 높일 뿐만아니라 디버깅을 쉽게할 수 있는 장점이 있다.


강력한 객체지향 프로그래밍 지원

인터페이스, 제네릭 등과 같은 강력한 객체지향 프로그래밍 지원은 크고 복잡한 프로젝트의 코드 기반을 쉽게 구성할 수 있도록 도우며 Java, C#등 클래스 기반 객체지향 언어에 익숙한 개발자가 자바스크립트 프로젝트를 수행하는 데 진입장 벽을 낮추는 효과도 있다.


ES6 / ESNext 지원

TypeScript는 별도의 개발환경을 구축해야 하므로 다소 복잡해진 측면이 있지만 ES6를 완전히 지원하지 않고 있는 브라우저들을 고려하여 Babel등의 트랜스파일러를 사용해야하는 현재의 상황임을 감안하면 TypeScript를 위한 환경 구축에 드는 수고는 크지 않음(트랜스파일러든, TypeScript 컴파일러든 어쨋든 한가지는 해야하는 상황이란 이야기)
또한 TypeScript는 ECMAScript 표준에 포함되지는 않았지만 표준화가 유력한 스펙을 선제적으로 도입하므로 유용한 기능을 안전하게 도입하기에 유리하다 (이건 babel도 마찬가지긴 함)


4.3 TypeScript 개발환경 구축

TypeScript의 파일(.ts)은 브라우저에서 동작하지 않으므로 TypeScript 컴파일러를 이용해 자바스크립트 파일로 변환해야 한다.


4.3.1 TypeScript 컴파일러 설치

npm을 사용하여 TypeScript을 전역에 설치

1
npm install -g typescript

버전확인

1
tsc -v


4.3.2 TypeScript 컴파일러 사용법

TypeScript 컴파일러(tsc)는 .ts파일을 .js파일로 컴파일(트랜스파일링) 한다.
(TypeScript 파일을 자바스크립트 파일로 변환하는 과정은 컴파일보다는 트랜스파일링이 더 적절한 표현이라고 함)

person.ts의 파일의 변환(확장자는 생략해도 무방)

1
tsc person

person.ts / student.ts 복수의 파일 변환

1
tsc person student

와일드 카드를 사용하여 모든 TypeScript의 파일 변환

1
tsc *.ts

watch를 통한 자동변환(–watch / -w)
(지정한 파일에 내용 변경이 일어나면 자동으로 감지하여 변환)

1
tsc person --watch

트랜스파일링될 js의 버전지정(–target/ - t)
(별도의 선언 없이 트랜스파일링되는 .js파일의 버전은 ES3)

1
tsc person -t es6

설정할 수 있는 버전은 ES3, ES5, ES6(ES2015), ES2016, ES2017(ESNext)이며 저 자세한 옵션들은 ‘typeScript Compiler Options’ 참고

참고 - typeScript Compiler Options


4.4 정적 타이핑(static typing)

4.4.1 타입 선언

변수에서의 타입 선언은 변수명 뒤에 타입(자료형, data type)을 명시하는 것으로 타입을 선언할 수 있다.

1
let foo:string = 'hello';

함수에서의 타입은 아래와 같이 선언

1
2
3
4
5
6
7
// 매개변수(x, y)와 반환값에 대한 타입 설정
function multiply(x: number, y: number): number {
return x * y;
}

// arrow function
const multiply2 = (x: number, y: number): number => x * y;

만약 선언한 타입에 맞지 않는 값을 할당하면 컴파일 시점에 에러가 발생하며 이는 변수, 함수 모두 동일

1
2
3
4
let bar: number = true;

multiply('1', 10);
multiply2(2, true);

타입 선언은 개발자가 코드를 예측할 수 있도록 돕는 것과 동시에 문법이나 타입 에러 혹은 일치하지 않는 값의 할당등 기본 오류를 런타임 이전에 검출하며
IDE(EX>vscode)에 따라 코드를 작성하는 시점에도 에러를 검출할 수 있는 등 개발효율을 크게 향상시켜 줄 수 있다.

아래와 같이 TypeScript는 자바스크립트의 superset으로 자바스크립트의 타입을 그대로 사용할 수 있을 뿐 아니라 typeScript 고유의 타입을 추가로 사용할 수 있음

JavaScript와 TypeScript 함께 사용할 수 있는 데이터 타입

타입 (data type) 설명
boolean 논리형 (true / false)
null 값이 없음을 명시
undefined 값을 할당하지 않은 변수의 초깃값
number 숫자(정수, 실수, Infinity, NaN)
string 문자열
symbol 고유하고 수정 불가능한 데이터 타입이며 주로 객체 프로퍼티의 식별자로 사용
(ES6에서 추가된 타입으로 MDN 참고)
object 객체형
array 배열


TypeScript에서 사용할 수 있는 데이터 타입

타입 (data type) 설명
tuple 고정된 요소 수만큼의 자료형을 미리 선언 후 배열을 표현
enum 열거형, 숫자 값 집합에 이름을 지정한 것
any 타입을 추론할 수 없거나 타입 체크가 필요없는 변수에 사용, var키워드로 선언한 변수와 같이 어떤 타입의 값이라도 할당 가능
void 일반적으로 함수에서 반환값이 없을 때 사용
never 결코 발생하지 않는 값


참고 - TypeScript 기본타입


TypeScript에서 타입 선언의 예

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// boolean
let isDone: boolean: true;


// null
let n: null = null;


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


// string
let name: string = 'Kim';
name = 'Lee';
let greeting: string = `Hello my name is ${name}`; // ES6 템플릿 문자열


// object
let obj: object = {};


// array
let list1: any[] = [1, 'two', true];
let list2: number[] = [1, 3, 2];
let list3: Array<number> = [1, 2, 3]; // 제네릭 배열 타입


// tuple - 고정된 요소의 수만큼 타입을 미리 선언 후 배열을 표현
let tuple: [string, number];
tuple = ['hello', 20]; // ok!
tuple = [10, 'hello']; // error!
tuple = ['hello', 10, true]; // error!
tuple.push(true); // error!


// enum - enum(열거형)은 숫자 값 집합에 이름을 지정한 것
enum Color1 {Red, Green, Blue} // 값을 지정하지 않으면 0부터 시작되어 순차적으로 증가(Red = 0, Green = 1, Blue = 2)
let c11: Color1 = Color1.Red; // 0
let c12: Color1 = Color1.Green; // 1
let c13: Color1 = Color1.Blue; // 2

enum Color2 {Red = 4, Green, Blue} // 값을 지정한 요소의 다음 요소들은 순차적으로 증가
let c21: Color2 = Color2.Red; // 4
let c22: Color2 = Color2.Green; // 5
let c23: Color2 = Color2.Blue; // 6

enum Color3 {Red, Green = 4, Blue} // 초기 값이 지정되지 않은 첫번째 요소는 0
let c21: Color2 = Color2.Red; // 0
let c22: Color2 = Color2.Green; // 4
let c23: Color2 = Color2.Blue; // 5

enum Color4 {Red = 4, Green = 72, Blue = 999} // 요소의 값은 꼭 순차적이지 않아도 상관없음
let c21: Color2 = Color2.Red; // 4
let c22: Color2 = Color2.Green; // 72
let c23: Color2 = Color2.Blue; // 999


// any - 타입 추론을 할 수 없거나 체크가 필요없는 변수에 사용
let notSure: any = 10;
notSure = 'hello'; // ok!
notSure = true; // ok!


// void - 일반적으로 함수에서 반환 값이 없을 때 사용
let func1 = function(x:number):void {
console.log(x);
}
let func2 = (x:number):void => {
console.log(x);
};


// never
// - 결코 발생하지 않는 값, 어떤 값도 할당할 수 없음
// - 함수에 사용하는 어떤 값도 리턴되지 않는 것을 넘어 도달 불가능한 부분(에러 호출이나 무한루프)이 있어야 타입체크를 통과
let neverVar: never = null; // error!

function alwaysError(): never {
throw new Error(); // ok!
}

function infiniteLoop(): never {
while(true) {
console.log('inifinite'); // ok!
}
}

또한 TypeScript가 기본적으로 제공하는 타입은 모두 소문자로 대문자로 시작하는 타입의 표기는 타입의 래퍼객체 타입을 의미하므로 사용에 주의 (EX> string(타입), String(래퍼객체타입))


1
2
3
4
5
6
7
let primiteveStr: string;
primiteveStr = 'hello'; // ok!
primiteveStr = new String('hello!'); // error!

let objectStr: String;
objectStr = 'hello'; // ok!
objectStr = new String('hello') // ok!

위의 예시의 데이터 객체 외에 다른 객체의 유형도 타입이 될 수 있다.


1
2
3
4
5
6
7
8
9
// Date 타입
const today: Data = new Date();

// HTMLElement 타입
const elem: HTMLElement = document.getElementById('myId');

// 사용자 정의 타입(class)
class Person {}
const person: Person = new Person();


4.4.2 정적 타이핑(static typing)

정적 타이핑은 변수를 선언할 때 변수에 할당할 값의 타입에 따라 사전에 타입을 명시적으로 선언해야하며 선언한 타입에 맞는 값을 할당해야 함.

자바스크립트는 동적 타입(dynamic type)언어 혹은 느슨한 타입(loosely typed)언어로 변수 타입 선언 없이 값이 할당되는 과정에서 동적으로 타입을 추론하며 변수의 타입이 결정된 후에도 같은 변수에 여러 타입의 값을 교차하여 할당할 수 있음 이러한 동적 타이핑은 사용하기 간편하지만 코드를 예측하기 힘들어 예상치 못한 오류를 만들 가능성이 높음

1
2
3
4
5
6
7
8
// javascript(dynamic type)
let foo; // undefined
foo = null; // null
foo = {}; // object
foo = 3; // number
foo = 3.14; // number
foo = 'hello'; // string
foo = true; // boolean

정적 타이핑은 TypeScript의 가장 독특한 특징으로 타입을 명시적으로 선언하며 타입이 결정된 후에는 타입을 변경할 수 없다. 잘못된 타입의 값이 할당, 반환되면 컴파일러는 이를 감지하여 에러를 발생시킨다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// TypeScript(static type)
let foo: string,
bar: number,
baz: boolean;

foo = 'hello'; // ok!
bar = 123; // ok!
baz = 'true' // error!


// 함수에서의 사용
let add = (x: number, y: number): number => x + y;

add(10, 20); // 30
add('10', 20) // error!

정적, 동적 타이핑의 우위에 대한 것은 절대적으로 평가할 수 없지만 정적 타이핑의 장점인 코드 가독성, 예측성, 안정성은 대규모 프로젝트에 매우 적합하다.


4.4.3 타입 추론(type inference)

TypeScript에서도 타입 선언을 생략하면 값이 할당되는 과정에서 any로 설정되어 동적으로 타입이 결정된다. 하지만 이러한 방식은 TypeScript의 장점을 없애기 때문에 사용하지 않는 것을 권장

1
2
3
let foo;
foo = 'hello'; // string
foo = true // boolean


4.5 클래스

ES6에서 도입된 클래스는 클래스 기반의 언어에 익숙한 개발자가 보다 빠르게 학습할 수 있는 단순명료한 새로운 문법을 제시하고 있다.
(클래스는 사실 새로운 모델을 제공하는 것은 아니고 클래스도 함수로 기존의 프로토타입 기반 패턴의 문법적 설탕이다)
TypeScript에서의 클래스는 ES6의 클래스와 상당히 유사하지만 몇 가지 고유한 확장기능을 가지고 있다.


4.5.1 클래스 정의

ES6의 클래스는 아래와 같이 클래스 몸체에 프로퍼티를 선언할 수 없고 반드시 생성자(constructor) 내부에서 클래스 프로퍼티를 선언하고 초기화 한다.

1
2
3
4
5
6
7
8
9
10
// javaScript
class Person {
constructor(name) {
this.name = name;
}

walk() {
console.log(`${this.name} is walking`);
}
}

위 코드는 ES6에서는 문제없이 실행되지만 TypeScript에서는 컴파일에러가 발생한다.


TypeScript에서는 클래스 몸체에 클래스 프로퍼티를 사전에 선언해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// typeScript
class Person {
name: string;

constructor(name: string) {
this.name = name;
}

walk() {
console.log(`${this.name} is walking`);
}
}

const person = new Person('Kim');
person.walk(); // 'Lee is walking'


4.5.2 접근제한자

es6의 class와 달리 클래스 기반의 다른 언어처럼 TypeScript의 class에서는 접근제한자를 지원한다.

TypeScript의 class에서 지원하는 접근제한자

  • public(접근제한자를 생략하면 public으로 선언)
  • protected
  • private

접근제한자를 선언한 프로퍼티와 메서드의 접근 가능성

접근 가능성 public protected private
클래스 내부 O O O
자식 클래스 내부 O O X
클래스 인스턴스 O X X
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Foo
class Foo {
public x: string; // public은 생략해도 무방
protected y: string;
private z: string;

constructor(x: string, y: string, z: string) {
this.x = x;
this.y = y;
this.z = z;
}
}

const foo = new Foo('x', 'y', 'z');

console.log(x); // ok!
console.log(y); // error!
console.log(z); // error!


// Bar
class Bar extends Foo {
constructor(x: string, y: string, z: string) {
super(x, y, z);

console.log(this.x); // ok!(public)
console.log(this.y); // ok!(protected)
console.log(this.z); // error!(private)
}
}


4.5.3 생성자 파라미터에 접근제한자 선언

접근 제한자는 생성자 파라미터에도 선언할 수 있다. 이때 접근 제한자가 사용된 생성자 파라미터는 암묵적으로 클래스 프로퍼티로 선언되고 생성자 내부에서 별도의 초기화가 없어도 암묵적으로 초기화가 수행된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Foo {
constructor(public x: string) {}
}

const foo = new Foo('hello~!');
console.dir(foo);
console.log(foo.x); // ok!('hello')



class Bar {
constructor(private x: string) {}
}
const bar = new Bar('hi~!');
console.dir(bar);
console.log(bar.x); // error!

생성자 내부에 별도의 접근 제한자를 선언하지 않으면 생성자 파라미터는 생성자 내부에서만 유효한 지역변수가 되어 외부 참조가 불가능해진다.

1
2
3
4
5
6
class Foo {
constructor(x: string) {}
}

const foo = new Foo('hello~!');
console.dir(foo); // x프로퍼티가 존재하지 않음


4.5.4 readonly 키워드

TypeScript는 readonly 키워드를 사용할 수 있는데 readonly가 선언된 클래스 프로퍼티는 선언 시 또는 생성자 내부에서만 값을 할당할 수 있게 된다.
그 외에 경우에는 값을 할당할 수 없고 오직 읽기만 가능한 상태가 되며 이를 이용하여 상수의 선언에 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Foo {
// readonly로 클래스 프로퍼티 생성
private readonly MSG: string = 'hello';

constructor() {
// 생성자 내부에서는 readonly 프로퍼티도 할당가능
this.MSG = 'ho';
}

log() {
// 생성자와 선언 시점이 아닌 곳에서 값을 할당하는 경우 컴파일 시점에서 에러
//this.MSG = 'hi!';
console.log(this.MSG);
}
}

const foo = new Foo();
foo.log();


4.5.5 static 키워드

ES6에서의 static 키워드는 정적 메서드를 정의하는데 사용된다.
정적 메서드는 클래스의 인스턴스가 아닌 클래스 이름으로 호출하며 그렇기 때문에 인스턴스를 생성하지 않아도 호출할 수 있음

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// javascript
class Foo {
constructor(prop) {
this.prop = prop;
}

static staticMethod() {
// static 키워드로 정의한 정적 메서드에서는 this를 사용할 수 없음
// 정적 메서드 내부에서의 this는 클래스의 인스턴스가 아닌 자신을 가리킴
return 'static method';
}

prototypeMethod() {
return this.prop;
}
}

console.log(Foo.staticMethod()); // 'static method'

const foo = new Foo(123);
console.log(foo.staticMethod()) // error!


TypeScript에서는 static 키워드를 클래스 프로퍼티에도 사용할 수 있다. 정적 클래스 프로퍼티는 정적 메서드와 마찬가지로 인스턴스가 아닌 클래스 이름으로 호출하며 클래스의 인스턴스를 생성하지 않아도 호출할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// TypeScript
class Foo {
// 생성된 인스턴스의 개수
static instanceCounter: number = 0;

constructor() {
// 인스턴스가 생성될 때마다 1씩 증가
this.instanceCounter++;
}
}

// instanceCounter을 증가시키기 위해 인스턴스 생성
let foo = new Foo();
let foo2 = new Foo();

console.log(Foo.instanceCounter); // 2
console.log(foo.instanceCounter); // error!


4.5.6 추상 클래스(abstract)

추상 클래스(abstract class)는 abstract 키워드를 사용하며 선언하며 하나 이상의 추상 메서드를 포함한다. 직접 인스턴스를 생성할 수 없고 상속만을 위해 사용된다.
추상 클래스를 상속한 클래스는 추상 클래스의 추상 메서드를 반드시 구현해야한다.
4.5.7의 인터페이스(interface)는 추상 클래스와 유사하지만 모든 메서드가 추상 메서드라는 차이가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 추상 클래스
abstract class Animal {
// 추상 메서드
// 메서드는 프로퍼티와 달리 ()를 표기
abstract makeSound(): void;

move(): void {
console.log('roaming the earth');
}
}

class Dog extends Animal {
// Animal을 상속 받았으므로 추상 메서드인 makeSound는 반드시 구현해야 함
makeSound() {
console.log('bowwow~');
}
}

// const animal = new Animal(); // error!(추상 클래스는 인스턴스를 직접 생성할 수 없음)

const myDog = new Dog();
myDog.makeSound(); // 'bowwow~'
myDog.move(); // 'roaming the earth'


4.6 인터페이스(interface)

인터페이스는 일반적으로 타입 체크를 위해 사용되며 변수, 함수, 클래스에 사용할 수 있다.
인터페이스는 여러 가지 자료형을 갖는 프로퍼티로 이루어진 새로운 자료형을 정의하는 것과 유사한데 인터페이스에 선언된 프로퍼티 또는 메소드의 구현을 강제하여 일관성을 유지할 수 있도록 하는 것
인터페이스는 프로퍼티와 메서드를 가질 수 있다는 점에서 클래스와 유사하나 직접 인스턴스를 생성할 수 없고 모든 메서드가 추상 메서드이다.(가상 클래스는 일반 메서드도 생성할 수 있음) 그리고 추상 메서드와 달리 abstract 키워드를 사용하지 않는다.


4.6.1 변수와 인터페이스

인터페이스는 변수의 타입으로 사용할 수 있는데 이때 인터페이스를 타입으로 선언한 변수는 해당 인터페이스를 준수해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Todo {
id: number;
content: string,
completed: boolean
}

// 변수 todo의 type을 interface인 Todo로 설정
let todo:Todo

// 변수의 값은 설정한 인터페이스를 준수해야함
todo = {
id: 1,
content: 'typeScript',
completed: false
};

인터페이스를 사용하여 함수 파라미터의 타입도 선언할 수 있는 데 당연히도 해당 함수는 함수 파라미터의 타입으로 지정한 인터페이스를 준수하는 인수를 전달해야 한다.
함수의 객체를 전달할 때 복잡한 매개변수 체크가 필요 없어서 유용하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Todo {
id: number;
comtent: string;
completed: boolean;
}

let todos: Todo[] = [];

// 함수 addTodo의 파라미터 type을 Todo로 설정
function addTodo(todo: Todo) {
// ...은 rest parameter
todos = [...todos, todo];
}

const newTodo: Todo = {
id: 1,
content: 'typeScript',
completed: false
};

addTodo(newTodo);
console.log(todos); // { id: 1, content: 'typeScript', completed: false }

참고 - rest parameter(MDN)


4.6.2 함수와 인터페이스

인터페이스는 함수의 타입으로도 사용할 수 있다. 이때 함수의 인터페이스에는 타입이 선언된 파라미터와 리턴 타입을 정의한다.

1
2
3
4
5
6
7
8
9
10
11
// 인터페이스 SquareFunc은 함수의 타입
interface SquareFunc {
// 파라미터 및 반환값의 type정의
(num: number): number;
}

const squereFunc: SquereFunc = function(num: number) {
return num * num;
}

console.log(squereFunc(10)); // 100


4.6.3 클래스와 인터페이스

클래스 선언문의 implements뒤에 인터페이스를 선언하면 해당 클래스는 지정된 인터페이스를 반드시 구현하여야 한다. 이는 인터페이스를 구현하는 클래스의 일관성을 유지할 수 있다는 장점을 갖는다.
(인터페이스는 프로퍼티와 메소드를 가질 수 있다는 점에서 클래스와 유사하나 직접 인스턴스를 생성할 수는 없다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 인터페이스 ITodo 정의
interface ITodo {
id: number;
content: string;
completed: boolean;
}

class Todo implements ITodo {
constructor(
public id: number,
public content: string,
public completed: boolean
) {}
}

const todo = new Todo(1, 'typeScript', false);

console.log(todo); // { id: 1, content: 'typeScript', completed: false }

인터페이스는 프로퍼티 뿐만 아니라 메소드도 포함할 수 있다.
(단 모든 메소드는 추상메소드이어야 하며 프로퍼티와 마찬가지로 인터페이스에서 정의한 추상 메소드는 반드시 구현하여야 한다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface IPerson {
name: string;
sayHello(): void;
}

class Person implements Iperson {
constructor (
public name: string
) {}

sayHello() {
console.log(`Hi~ ${this.name}`);
}
}

function greeter(person: IPerson) {
person.sayHello();
}

const me = new Person('kim');

greeter(me); // 'Hi~ kim'


4.6.4 덕 타이핑(duck typing)

주의해야 할 것은 인터페이스를 구현하였다는 것만이 타입 체크를 통과하는 유일한 방법은 아니다. 타입 체크에서 중요한 것은 값을 실제로 가지고 있다는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 1. 인터페이스 IDuck을 정의
interface IDuck {
quack():void;
}

// 3. 클래스 MallardDuck은 인터페이스 IDuck을 구현
class MallardDuck implements IDuck {
quack() {
console.log('Quack!');
}
}

// 4. 클래스 RedheadDuck은 인터페이스 IDuck을 구현하지는 않았지만 인터페이스 IDuck과 동일한 구조를 가짐
class RedheadDuck {
quack() {
console.log('Q~uack!');
}
}

// 2. 함수 makeNoise는 매개변수로 duck(IDuck의 인터페이스를 구현한)을 전달받음
function makeNoise(duck: IDuck) {
duck.quack();
}


makeNoise(new MallardDuck()); // 'Quack!'

// 5. IDuck 인터페이스를 구현하지 않는 매개변수를 사용해도 이상없이 동작
makeNoise(new RedheadDuck()); // 'Q~uack!'
  1. 인터페이스 IDuck은 quack 메서드를 정의
  2. 함수 makeNoise는 매개변수로 duck(IDuck의 인터페이스를 구현한)을 전달받음
  3. 클래스 MallardDuck은 인터페이스 IDuck을 구현
  4. 클래스 RedheadDuck은 인터페이스 IDuck을 구현하지는 않았지만 인터페이스 IDuck과 동일한 구조를 가짐
  5. IDuck 인터페이스를 구현하지 않는 매개변수를 사용해도 이상없이 동작

TypeScript는 해당 인터페이스에서 정의한 프로퍼티나 메서드를 가지고 있다면 그 인터페이스를 구현한 것으로 인정한다.
이 것을 덕 타이핑(duck typing) 또는 구조적 타이핑이라고 한다. 인터페이스를 변수에 사용할 경우에도 덕 타이핑은 적용된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface IPerson {
name:string;
}

function sayHello(person: IPerson) {
console.log(`hello~ ${person.name}`);
}

// 인터페이스 IPerson을 구현하지 않았음
const me = {
name: 'Lee',
age: 18
};

sayHello(me); // 'hello~ Lee'

인터페이스는 개발 단계에서 도움을 주기 위해 제공되는 기능으로 자바스크립트의 표준이 아니다. 위 예제의 TypeScript 파일을 자바스크립트 파일로 트랜스파일링하면 아래와 같이 인터페이스가 삭제된다.


4.6.5 선택적 프로퍼티(optional property)

인터페이스 프로퍼티는 반드시 구현되어야 한다. 하지만 인터페이스의 프로퍼티가 선택적으로 필요한 경우가 있을 수 있는데 이때 선택적 프로퍼티로 선언할 수 있다.
선택적 프로퍼티는 프로퍼티명 뒤에 ‘?’를 붙여 사용하며 생략하여도 에러가 발생하지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 인터페이스 UserInfo의 age, address 프로퍼티는 생략 가능하다.
interface UserInfo {
userName: string;
passWord: string;
age? : number;
address?: string
};

const userInfo: UserInfo = {
userName: 'aaa@asd.com',
password: 'q1w2e3'
};

console.log(userInfo);


4.6.7 제네릭

정적 타입 언어는 함수 또는 클래스를 정의하는 시점에 매개변수나 반환 값의 타입을 선언하여야 한다. 하지만 함수 또는 클래스를 정의하는 시점에 매개변수, 반환 값의 타입을 지정하기 어려운 경우가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Queue {
protected data = [];

push(item) {
this.data.push(item);
}

pop() {
return this.data.shift();
}
}

const queue = new Queue();

queue.push(0);
queue.push('1'); // 의도하지 않은 실수(data에 들어가는 모든 값은 숫자일 것으로 예상)

console.log(queue.pop().toFixed()) // 0
console.log(queue.pop().toFixed()) // error! (toFixed는 Number의 메서드이므로)

위 예제는 FIFO(First In First Out)구조로 데이터를 저장하는 큐를 표현한 것으로 data 프로퍼티에 타입 선언을 생략하여 any[] 타입으로 설정되었으며 any[] 타입은 어떤 타입의 요소도 가질 수 있다는 것을 의미한다.
즉 저장하는 값은 어떤 타입의 값이던 포함할 수 있게되므로 숫자만 들어올 것이라는 기대를 충족하지 못하게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Queue {
protected data = [];

push(item) {
this.data.push(item);
}

pop() {
return this.data.shift();
}
}

class NumberQueue extends Queue {
push(item: number) {
super.push(item);
}

pop() {
return super.pop();
}

}

const queue = new NumberQueue();

queue.push(0);
// queue.push('1'); // 컴파일 과정에서 에러 발생
queue.push(+'1');

console.log(queue.pop().toFixed()) // 0
console.log(queue.pop().toFixed()) // 1

이러한 문제를 해결하기 위해 Queue를 상속 받는 number타입 전용의 클래스를 정의하였다. 하지만 다양한 타입을 지원해야 한다면 타입 별로 클래스를 상속 받아 추가해야하므로 이 또한 좋은 방법은 아니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Queue<T> {
protected data: Array<T> = [];

push(item: T) {
this.data.push(item);
}

pop() {
return this.data.shift();
}
}



// 숫자 전용 Queue
const numberQueue = new Queue<number>();

numberQueue.push(0);
// queue.push('1'); // 컴파일 과정에서 에러 발생
queue.push(+'1');

console.log(queue.pop().toFixed()) // 0
console.log(queue.pop().toFixed()) // 1



// 문자열 전용 Queue
const stringQueue = new Queue<string>();

stringQueue.push('hello');
stringQueue.push('world');

console.log(queue.pop().toFixed()) // 'hello'
console.log(queue.pop().toFixed()) // 'world'



// 커스텀 객체 전용 Queue
const myQueue = new Queue<{
name: string,
age: number
}>;

myQueue.push({
name: 'Lee',
age: 20
});
myQueue.push({
name: 'kim',
age: 40
});

console.log(myQueue.pop()); // {name: 'Lee', age: 20}
console.log(myQueue.pop()); // {name: 'kim', age: 40}

제네릭은 선언 시점이 아닌 생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법이다. 한번의 선언으로 다양한 타입에 재사용이 가능하다는 장점이 있다.
T는 제네릭을 선언할 때 관용적으로 사용되는 식별자로 ‘타입 파라미터(type parameter)라 한다.

1
2
3
4
5
6
7
8
9
10
function reverse<T>(items: T[]): T[] {
return item.reverse();
}

const arg = [1, 2, 3, 4, 5];

// 인수에 따라 타입 매개변수가 결정
const reserved = reverse(arg);

console.log(reversed); // [5, 4, 3, 2, 1]

위와 같이 함수에서도 제네릭을 사용할 수 있는 데 reverse 함수는 인수의 타입에 의해 매개변수가 결정된다.
예를 들어 위와 같이 number타입의 요소를 갖는 배열을 전달받으면 타입 매개변수가 number가 된다.

1
2
3
4
5
6
7
8
9
10
11
function reverse<T>(items: T[]): T[] {
return item.reverse();
}

const arg = [
{name: 'Lee'},
{name: 'kim'},
];

const revesed = reverse(arg);
console.log(reversed); // [ {name: 'kim'}, {name: 'Lee'} ]

위와 같이 name의 타입이 string을 갖는 요소의 배열을 전달 받으면 타입 매개변수는 { name: string } 이 된다.

참고


4.6.8 데코레이터(Decorator)

TypeScript에서 ‘@’라는 문자로 사용하는 문법을 데코레이터(Decorator)라고 하며 코드의 조각을 장식해주는 역활을 합니다.
데코레이터는 클래스 선언, 메서드 선언, 접근 제한자, 속성 또는 매개변수에 첨부할 수 있는 특별한 종류의 선언입니다.

참조

데코레이터는 @decorator과 같이 사용할 수 있으며 @[name]의 형식일 때 ‘name’에 해당하는 이름의 함수를 참조하게 됩니다.
(즉 데코레이터의 이름이 Component라면 @Component로 사용)

실행 시점

데코레이터로 정의된 함수는 메코레이터가 적용된 메서드가 실행되거나 클래스가 new 키워드를 통해 인스턴스화 될때가 아닌 런타임때 실행됩니다. (즉 매번 실행되지 않음)


Decorator To Method(메서드에 적용되는 경우)
1
2
3
4
5
6
// decorator.js
function chaining(target: any, key: string, descriptor: PropertyDescriptor): any {
console.log(target);
console.log(key);
console.log(descriptor);
}

위 함수는 추후 메서드에 @chaining 형식으로 사용될 함수로 @과 함께 함수가 호출되는 경우 받게되는 파라미터는 아래와 같습니다. (이는 Object.defineProperty()를 통해 이를 정의하고 있기 때문)

target - 속성을 정의하고자 하는 객체
name - 새로 정의하거나 수정하려는 속성의 이름
descriptor - 새로 정의하거나 수정하려는 속성에 대해 기술하는 객체

1
2
3
4
5
6
7
8
9
// decorator.js(메서드에 적용되는 데코레이터로 체이닝 기능을 구현)
function chaining(target: any, key: string, descriptor: PropertyDescriptor): any {
const fn: Funtion = descriptor.value;

descriptor.value = function(...args: any[]) {
fn.apply(target, args);
return target;
}
}

descriptor의 value가 데코레이터가 적용된 함수, 즉 실행 대상이라고 할 수 있습니다.
descriptor.value를 재정의(override)하기 전에 변수 fn에 cahing 해둔 다음 호출한 후의 일을 정의하기 위해 호출한 후의 일을 정의하기 위해 위와 같이 재정의해줍니다.
( 재정의하기 전 caching 해둔 함수를 호출하기 위해서 apply 메서드를 사용했으며 어떠한 매개변수가 얼만큼 전달될지 모르지 rest parameter를 통해 fn을 호출해주는 코드 )

위와 같이 descriptor.value가 재정의되면 chaining이 적용된 메서드는 재정의된대로 호출하게 됩니다.

1
2
3
4
5
6
7
8
// class
class Pet {
@chaining
bark() {
}
}

const per = new Pet();

위와 같이 적용하면 클래스 Pet에서 메서드 bark은 Pet.prototype.bark가 되는 데 class syntax내부에서 그 전에 decorator 함수가 실행되어 본래 bark이라는 메서드에서 정의된 것에 추가적인 장식을 더 해 prototype에 추가되도록 합니다.
(즉 클래스 Pet의 메서드 back이 Pet.prototype.bark이 되기 전에 데코레이터가 실행되어 추가적인 장식을 더해 Pet.prototype.bark이 되도록 함)

1
pet.bark().bark();

위와 같이 실행하면 데코레이터의 효과로 return this;를 하지 않아도 chaining 기능을 사용하여 메서드를 호출할 수 있게 됩니다.

Pet을 기준으로 데코레이터에서 받는 파라미터를 출력 해보면 아래와 같습니다.

1
2
3
4
5
6
// decorator.js
function chaining(target: any, key: string, descriptor: PropertyDescriptor): any {
console.log(target); // {bark: f, constructor: f}
console.log(key); // bark
console.log(descriptor); // {value: f, writable: true, enumerable: true, configurable: true}
}


Decorator To Class(클래스에 적용되는 경우)

데코레이터가 class에 적용되는 경우 그 signature(특징)가 조금 달라집니다.
클래스 데코레이터는 클래스 선언 바로 선언되며 클래스 생성자(constructor)에 적용되어 클래스 정의를 관찰, 수정 또는 대체하도록 사용할 수 있습니다.

1
2
3
4
5
function component(target, key, descriptor) {
console.log(target); // ...
console.log(key); // undefined
console.log(descriptor); // undefined
}

메서드에 데코레이터를 적용하듯 데코레이터 함수를 선언하면 올바른 선언을 할 수 없습니다.
클래스에 적용되는 데코레이터 함수에 전달되는 인자는 constructor 하나이며 아래와 같이 선언 합니다.

1
2
3
4
5
function classDecorator<T extends { new(...args:any[]): {} }>(constructor: T) {
return class extends constructor {
newProperty = 'new property';
}
}

런타임에 데코레이터를 적용하는 로직은 이를 수행하지 않기 때문에 클래스에 적용되는 데코레이터 함수 내에서 새로운 생성자 함수를 반환하면 원래 프로토타입을 유지해야 합니다.
위 코드에서는 기존의 프로토타입을 유지하기 위해 적용되는 클래스의 constructor를 extends 합니다.

출처 - JaeYeop Han - (TS)6. Decorator