$ mkdir ts-practice # ts-practice 라는 디렉터리 생성
$ cd ts-practice # 해당 디렉터리로 이동
$ yarn init -y # 또는 npm init -y
이렇게 하면 ts-practice라는 디렉터리에 package.json 파일이 생성됨.
타입스크립트 설정파일 tsconfig.json을 프로젝트 디렉터리 안에 생성한다. (직접 입력해서 만들 수도 있음.)
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true
}
}
하지만 일반적으로 명령어로 생성한다.
$ yarn global add typescript
이후 프로젝트 디렉터리 안에서 "tsc --init" 명령어를 입력하면 tsconfig.json 파일이 자동생성 된다.
이 파일에서는 타입스크립트가 지정될 때 필요한 옵션들을 지정한다.
간단하게 정리해보면 다음과 같다.
여기에 한 가지 속성을 더 추가해 보자. outDir라는 속성인데, 이를 설정하면 컴파일된 파일들이 저장되는 경로를 지정 할 수 있다.
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"outDir": "./dist"
}
}
프로젝트에 src 디렉터리를 만들고, 그 안에 practice.ts 라는 파일을 만들어보자.
const message: string = 'hello world'; // : string은 해당 변수가 문자열이라는 것을 명시해줌.
console.log(message);
타입스크립트는 이렇게 ".ts" 확장자를 사용한다.
message 값이 선언된 코드를 보면, : string 이라는 코드가 있는데, 이 코드가 해당 상수 값(message)이 문자열이라는 것을 명시해 준다.
만약 해당 값을 숫자로 설정하면, 에디터 상의 오류가 나타난다.
코드를 모두 작성하셨으면 해당 프로젝트의 디렉터리에 위치한 터미널에서 tsc 명령어를 입력해보자.
그러면 dist/practice.js 경로에 다음과 같이 파일이 생성될 것이다.
"use strict";
var message = 'hello world';
console.log(message);
우리가 ts 파일에서 명시한 값의 타입은 컴파일이 되는 과정에서 모두 사라지게 된다.
이제 let과 const를 사용하여 특정 값을 선언할 때 여러가지 기본 타입을 지정하여 사용하는 것을 연습해보자.
src/practice.ts
let count = 0; // 숫자
count += 1;
count = '갑자기 분위기 문자열'; // 이러면 에러 발생
const message: string = 'hello world'; // 문자열
const done: boolean = true; // 불리언 값
const numbers: number[] = [1, 2, 3]; // 숫자 배열
const messages: string[] = ["Hello", "world"]; // 문자열 배열
messages.push(1); // 숫자를 넣으려고 하면 안됨.
let mightBeUndefined: string | undefined = undefined; // string일 수도 있고, undefined일 수도 있음
let nullableNumber: number | null = null; // number일 수도 있고, null일 수도 있음
let color: 'red' | 'orange' | 'yellow' = 'red'; // red, orange, yellow 중 하나임
color = yellow;
color = green; // 에러 발생
TypeScript를 사용하면 이렇게 특정 변수 또는 상수의 타입을 지정할 수 있고 우리가 사전에 지정한 타입이 아닌 값이 될 때 바로 에러를 발생시킨다.
이렇게 에러가 나타났을 때는 바로 컴파일이 되지 않는다. tsc 명령어를 입력해서 컴파일을 하려고 하면 다음과 같이 실패하게 된다.
이번에는 함수에서 타입을 지정하는 방법을 알아보자.
src/practice.ts
function sum(x: number, y: number): number {
return x + y;
}
sum(1, 2);
타입스크립트를 사용하면, 다음과 같이 코드를 작성하는 과정에서 함수의 파라미터로 어떤 타입을 넣어야 하는지 바로 알 수 있다.
위 코드의 첫 번째 줄의 가장 우측을 보면 :number가 있다. 이는 해당 함수의 결과물이 숫자라는 것을 명시해 준다.
만약에 이렇게 결과물이 number라는 것을 명시해 놓고 갑자기 null을 반환한다면 오류가 뜨게 된다.
이번에는 숫자 배열의 총합을 구하는 sumArray라는 함수를 작성해 보자.
function sumArray(numbers: number[]): number {
return numbers.reduce((acc, current) => acc + current, 0);
}
const total = sumArray([1, 2, 3, 4, 5]);
타입스크립트를 사용했을 때 편리한 점은, 배열의 내장함수를 사용할 때에도 타입 유추가 매우 잘 이루어 진다는 점이다.
참고로 함수에서 만약 아무것도 반환하지 않아야 한다면 반환 타입을 void로 설정하면 된다.
function returnNothing(): void {
console.log('I am just saying hello world');
}
interface는 클래스 또는 객체를 위한 타입을 지정할 때 사용되는 문법이다.
// Shape 라는 interface 를 선언합니다.
interface Shape {
getArea(): number; // Shape interface 에는 getArea 라는 함수가 꼭 있어야 하며 해당 함수의 반환값은 숫자입니다.
}
class Circle implements Shape {
// `implements` 키워드를 사용하여 해당 클래스가 Shape interface 의 조건을 충족하겠다는 것을 명시합니다.
radius: number; // 멤버 변수 radius 값을 설정합니다.
constructor(radius: number) {
this.radius = radius;
}
// 너비를 가져오는 함수를 구현합니다.
getArea() {
return this.radius * this.radius * Math.PI;
}
}
class Rectangle implements Shape {
width: number;
height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
const shapes: Shape[] = [new Circle(5), new Rectangle(10, 5)];
shapes.forEach(shape => {
console.log(shape.getArea());
});
여기까지 코드를 작성하고 tsc 명령어를 입력해 보자.
그 다음엔, node dist/practice 명령어를 입력하여 컴파일된 스크립트를 실행시켜 보자.
잘 작동한다.
우리가 기존에 작성했던 코드를 보면,
width: number;
height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
이런식으로 width, height 멤버 변수를 선언한 다음에 constructor 에서 해당 값들을 하나하나 설정해 주었는데, 타입스크립트에서는 constructor 의 파라미터 쪽에 public 또는 private accessor(접근자)를 사용해주면 직접 하나하나 설정해 주는 작업을 생략해 줄 수 있습니다.
// Shape 라는 interface 를 선언합니다.
interface Shape {
getArea(): number; // Shape interface 에는 getArea 라는 함수가 꼭 있어야 하며 해당 함수의 반환값은 숫자입니다.
}
class Circle implements Shape {
// `implements` 키워드를 사용하여 해당 클래스가 Shape interface 의 조건을 충족하겠다는 것을 명시합니다.
constructor(public radius: number) {
this.radius = radius;
}
// 너비를 가져오는 함수를 구현합니다.
getArea() {
return this.radius * this.radius * Math.PI;
}
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
const circle = new Circle(5);
const rectangle = new Rectangle(10, 5);
console.log(circle.radius);
console.log(rectangle.width);
const shapes: Shape[] = [new Circle(5), new Rectangle(10, 5)];
shapes.forEach(shape => {
console.log(shape.getArea());
});
public 으로 선언된 값은 클래스 외부에서 조회 할 수 있으며 private으로 선언된 값은 클래스 내부에서만 조회 할 수 있다. 따라서 위 코드에서는 circle 의 radius 값은 클래스 외부에서 조회 할 수 있지만, rectangle 의 width 또는 height 값은 클래스 외부에서 조회 할 수 없다.
이번에는 클래스가 아닌 일반 객체를 interface를 사용하여 타입을 지정하는 방법을 알아보도록 하자.
interface Person {
name: string;
age?: number; // 물음표가 들어갔다는 것은, 설정을 해도 되고 안해도 되는 값이라는 것을 의미합니다.
}
interface Developer {
name: string;
age?: number;
skills: string[];
}
const person: Person = {
name: '김사람',
age: 20
};
const expert: Developer = {
name: '김개발',
skills: ['javascript', 'react']
};
지금 보면 Person과 Developer가 형태가 유사한 것이 보인다. 이럴 땐 interface를 선언할 때 다른 interface를 extends 키워드를 사용해서 상속 받을 수 있다.
interface Person {
name: string;
age?: number; // 물음표가 들어갔다는 것은, 설정을 해도 되고 안해도 되는 값이라는 것을 의미합니다.
}
interface Developer extends Person {
skills: string[];
}
const person: Person = {
name: '김사람',
age: 20
};
const expert: Developer = {
name: '김개발',
skills: ['javascript', 'react']
};
const people: Person[] = [person, expert];
type은 특정 타입에 별칭을 붙이는 용도로 사용한다. 이를 사용하여 객체를 위한 타입을 설정할 수도 있고, 배열, 또는 그 어떤 타입이던 별칭을 지어줄 수 있다.
src/practice.js
type Person = {
name: string;
age?: number; // 물음표가 들어갔다는 것은, 설정을 해도 되고 안해도 되는 값이라는 것을 의미합니다.
};
// & 는 Intersection 으로서 두개 이상의 타입들을 합쳐줍니다.
// 참고: https://www.typescriptlang.org/docs/handbook/advanced-types.html#intersection-types
type Developer = Person & {
skills: string[];
};
const person: Person = {
name: '김사람'
};
const expert: Developer = {
name: '김개발',
skills: ['javascript', 'react']
};
type People = Person[]; // Person[] 를 이제 앞으로 People 이라는 타입으로 사용 할 수 있습니다.
const people: People = [person, expert];
type Color = 'red' | 'orange' | 'yellow';
const color: Color = 'red';
const colors: Color[] = ['red', 'orange'];
우리가 이번에 type 과 interface 를 배웠는데, 어떤 용도로 사용을 해야 할까? 클래스와 관련된 타입의 경우엔 interface 를 사용하는게 좋고, 일반 객체의 타입의 경우엔 그냥 type을 사용해도 무방하다. 사실 객체를 위한 타입을 정의할때 무엇이든 써도 상관 없는데 일관성 있게만 쓰면 된다고 한다.
이에 대한 자세한 내용은 다음 링크에 자세히 서술되어 있다.
제너릭(Generics)은 타입스크립트에서 함수, interface, type alias를 사용하게 될 때 여러 종류의 타입에 대하여 호환을 맞춰야 하는 상황에서 사용하는 문법이다.
예를 들어서 우리가 객체 A와 B를 합쳐주는 merge라는 함수를 만든다고 가정해보자. 그런 상황에서는 A와 B 가 어떤 타입이 올지 모르기 때문에 이런 상황에서는 any라는 타입을 쓸 수도 있다.
function merge(a: any, b: any): any {
return {
...a,
...b
};
}
const merged = merge({ foo: 1 }, { bar: 1 });
그런데, 이렇게 하면 타입 유추가 모두 깨진거나 다름이 없게 된다. 결과가 any 라는 것은 즉 merged 안에 무엇이 있는지 알 수 없다는 것이다.
function merge<A, B>(a: A, b: B): A & B {
return {
...a,
...b
};
}
const merged = merge({ foo: 1 }, { bar: 1 });
또 다른 예시도 알아보자.
function wrap<T>(param: T) {
return {
param
}
}
const wrapped = wrap(10);
이렇게 함수에서 Generics 를 사용하면 파라미터로 다양한 타입을 넣을 수도 있고 타입 지원을 지켜낼 수 있다.
이번엔 interface에서 Generics를 사용하는 방법에 대해 알아보자.
interface Items<T> {
list: T[];
}
const items: Items<string> = {
list: ['a', 'b', 'c']
};
방금 interface에서 Generics를 사용한 것과 매우 흡사하다.
type Items<T> = {
list: T[];
};
const items: Items<string> = {
list: ['a', 'b', 'c']
};
이번에는 클래스에서 자료형을 사용해보자.
Queue 라는 클래스를 만들어보자. Queue 는 데이터를 등록 할 수 있는 자료형이며, 먼저 등록(enqueue)한 항목을 먼저 뽑아올 수(dequeue) 있는 성질을 가지고 있다.
class Queue<T> {
list: T[] = [];
get length() {
return this.list.length;
}
enqueue(item: T) {
this.list.push(item);
}
dequeue() {
return this.list.shift();
}
}
const queue = new Queue<number>();
queue.enqueue(0);
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
queue.enqueue(4);
console.log(queue.dequeue());
console.log(queue.dequeue());
console.log(queue.dequeue());
console.log(queue.dequeue());
console.log(queue.dequeue());
이제 해당 코드를 컴파일하고 실행해보자.
$ tsc
$ node dist/practice
0
1
2
3
4
잘 작동한다면, 타입스크립트를 리액트와 함께 쓰기위한 준비를 마친 것이다. 다음부터는 본격적으로 리액트에서 타입스크립트를 사용하는 방법을 공부해 보도록 하겠다.