🔥 학습목표
- TypeScript 프로젝트를 생성할 수 있다.
- TypeScript의 사용 이유를 설명할 수 있다.
- TypeScript의 타입, 함수, 연산자를 직접 작성할 수 있다.
JavaScript에 정적타입 검사와 클래스 기반 객체 지향 프로그래밍 등의 기능을 추가하여 개발된 언어다.
└▷ 마이크로소프트에서 개발한 JavaScript의 상위 집합(Superset) 언어이다.
└▷ JavaScript가 발전하면서 생긴 단점을 보완하기 위해 등장하게 되었다.
JavsScript의 동적 타입은 유연하고 편리하지만, 타입의 명시성이 부족하다.
타입의 명시성이 정확하지 않은 경우, 자동 형변환이 일어나 1 + "2" = "12"
와 같은 연산을 멋대로 수행한다.
위와 같은 문제를 해결하기 위해
정적 타입 검사 기능을 제공한다.
코드의 가독성과 유지 보수성을 높여준다.
아래는 인터페이스를 활용한 TypeScript 사용 예시다.
interface User {
id: number;
name: string;
}
function greetingUser(user: User) {
console.log(`Hello, ${user.name}!`)
}
const parkUser = {
id: 1,
name: "박해커"
};
greetingUser(parkUser);
User 객체가 어떤 타입의 프로퍼티를 가지고 있는지 명시적으로 드러난다. greetingUser
함수 또한 어떤 타입의 매개변수를 인자로 받는지 정확히 명시한다.
프로젝트 폴더를 생성한다.
/* mkdir (폴더명) */
mkdir typescript-learn
/* cd (폴더명) */
cd typescript-learn
npm init -y
명령어를 실행하면 pakage.json
파일이 생성된다. 이제 프로젝트 내부에서 npm
을 사용할 준비가 된 것이다.
npm install typescript --save-dev
프로젝트가 생성되면 루트 디렉토리에 tsconfig.json
파일을 생성한다.
이 파일은 TypeScript 파일들을 JS 파일로 변환할 때 어떤 방식으로 변환할 것인지 세부 설정을 하는 용도다.
{
"compilerOptions": {
"target": "es6", // 어떤 버전의 자바스크립트로 바꿔줄 것인지 정한다.
"module": "commonjs", // JS파일 간 import 문법을 구현할 때 어떤 문법을 사용할 것인지 정한다.
"sourceMap": true, // 빌드 시 map 파일을 생성할 것인지에 대한 여부.
"outDir": "./dist" // JS 파일의 아웃풋 경로
},
"include": [
"src/**/*"
]
}
module - commonjs
는 require
을 사용, es2015' 'esnext
는 import
문법을 사용한다.
SourceMap - 코드 상의 위치를 기억하고 빌드 전 어떤 파일, 문장에서 오류가 났는지 확인할 수 있다. 자세한 설명은 여기
ESLint 설치
ESLint
를 설치한다.ctrl
+shift
+p
, 맥OS의 경우 cmd
+shift
+p
를 눌러 명령 팔레트로 이동한다.{
// ...
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.alwaysShowStatus": true,
"eslint.workingDirectories": [
{"mode": "auto"}
],
"eslint.validate": [
"javascript",
"typescript"
],
}
format on save
가 설정되어 있는지 확인하고, 설정을 해제한다.Prettier 설치
Prettier 확장 프로그램을 설치한다.
추가로 필요한 프리셋과 라이브러리를 설치한다.
npm i -D @babel/core @babel/preset-env @babel/preset-typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint prettier eslint-plugin-prettier
.eslintrc.js
파일을 만들고 아래 코드를 붙여 넣는다. prettier
와 typescript-eslint
, React 에 대한 설정 파일이다. (개발자의 취향에 따라 작성하면 된다.)module.exports = {
root: true, // 본 파일이 설정 파일이라는 걸 의미. 설정 파일을 찾기 위해 더 이상 상위 파일로 올라가지 않음.
env: { // ESLint는 미리 선언하지 않고 접근하는 변수에 대해 오류를 내기 때문에 기본 제공되는 전역 객체에 대해서 알려준다.
browser: true,
node: true,
jest: true,
},
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
], // 추천 설정을 사용
plugins: ['prettier', '@typescript-eslint'], // ESLint에서 기본으로 제공되는 규칙 외 추가적인 규칙 사용
rules: { // 규칙을 세세하게 제어. extends 옵션을 통해 설정된 규칙을 덮어쓰고 싶을 때 잘 쓰인다.
'prettier/prettier': [
'error',
{
singleQuote: true,
tabWidth: 2,
printWidth: 80,
bracketSpacing: true,
arrowParens: 'avoid',
},
],
'@typescript-eslint/no-explicit-any': 'off', //
'@typescript-eslint/explicit-function-return-type': 'off',
'prefer-const': 'off',
},
parserOptions: {// JS의 확장 문법이나 최신 문법으로 작성한 코드를 린트(lint)하기 위해서는 그에 상응하는 parser를 사용해야 한다.
parser: '@typescript-eslint/parser',
},
};
이제 src
폴더 밑에 index.ts
등과 같은 .ts
파일을 만들어서 TypeScript 코드를 작성할 수 있다.
컴파일이란, 소스코드를 특정 플랫폼에서 실행 가능한 형태로 변환하는 과정을 뜻한다.
브라우저에서 실행되는 자바스크립트는 전통적으로 컴파일이 필요 없는 인터프리터 언어다.
브라우저나 Node.js는 자바스크립트 코드를 그대로 이해하고 바로 실행할 수 있다.
하지만 타입스크립트는 어떨까?
타입스크립트로 작성한 코드는 자바스크립트로의 컴파일 과정이 필요하다.
개발은 타입스크립트로 하지만, 배포는 자바스크립트로 해야한다.
이때 우리는 타입스크립트 컴파일러(TypeScript compiler)를 사용한다.
컴파일러 실행 방법은 다음과 같다.
위에서 환경설정을 할 때 npm
을 통패 타입스크립트 컴파일러를 설치했었다.
만약 hello.ts
타입스크립트 파일을 작성했다면
npx tsc hello.ts
다음과 같이 tsc
커맨드를 사용하여 hello.ts
파일을 자바스크립트로 변환한다.
컴파일 결과 동일한 디렉토리에 hello.js
가 생긴 걸 볼 수 있다.
let isShow: boolean = true;
let isDone: boolean = false;
let number1: number = 5;
let number2: number = 0.7;
정수와 실수 구분 없이 Number
타입 하나로 표기한다.
let firstName: string = 'hello';
//첫 번째 방법
let items: string[] = ["apple", "banana", "grape"];
//두 번째 방법
let numberList: Array<number> = [4, 7, 100];
배열 타입은 기본적으로 하나의 타입만 작성하게 되어 있으며 혼용해서 작성하는 것은 불가능하다.
요소의 타입과 개수가 고정된 배열을 표현한다.
let user: [string, number, boolean] = ["kimcoding", 20, true];
특정 타입이 정해진 요소에 해당 타입과 호환되지 않는 접근을 할 경우 에러가 발생한다.
// 에러발생
console.log(user[2].toString());
TypeScript에서 객체란 JS와 마찬가지로 원시 타입(number
, string
, boolean
, undefined
, null
, symbol
)이 아닌 타입을 뜻한다.
let obj: object = {};
TypeScript에서 object
타입은 모든 객체를 수용하는 타입이다. 따라서 객체의 프로퍼티 타입을 별도로 지정해주지 않으면 any
로 지정되고 만다.
이는 타입의 안정성을 보장하는 TypeScript의 목적과 맞지 않기 때문에 객체의 프로퍼티 타입을 각기 명시해주어야 한다.
let user: {name: string, age: number} = {
name: "kimcoding",
age: 20
}
알지 못하는 타입을 표현할 때 사용한다.
유저→클라이언트 로 받은 데이터나 서드파티 라이브러리에서 들어오는 값은 개발자가 먼저 유추하기 어렵다.
이때 타입 검사를 하지 않기 위해 any
를 사용한다.
let maybe: any = 4;
any
타입은 변수를 재할당 할 때 타입에 구애받지 않고 받을 수 있다.
//에러 발생
let obj: string = "hello";
obj = 1;
//정상적으로 동작
let maybe: any = 4;
maybe = true;
또한 여러 타입이 섞인 배열을 받고자 하는데, 타입의 일부만 알고 전체를 알지 못할 때 any
를 사용하면 좋다.
let list: any[] = [1, true, "free"];
list[1] = 100;
any
이기 때문에 1번째 요소가 boolean
타입이더라도 number
타입으로 재할당할 수 있다.
//named function
function add(x: number, y: number):number {
return x + y;
}
//arrow function
let add = (x: number, y: number): number => {
return x + y;
}
만약 리턴 값이 없을 경우 void
를 사용한다.
let printAnswer = (): void => {
console.log("YES");
}
let greeting = (firstName: string, lastName: string): string => {
return `hello, ${firstName} ${lastName}`;
}
// 에러(매개변수 부족)
greeting('coding');
// 정상 작동
greeting('coding', 'kim');
// 에러(매개변수 과잉)
greeting('coding', 'kim', 'hacker');
let greeting = (firstName: string, lastName="kim"): string => {
return `hello, ${firstName} ${lastName}`;
}
// 정상 작동
greeting('coding');
// 정상 작동
greeting('coding', undefined);
// 에러(매개변수 과잉)
greeting('coding', 'kim', 'hacker');
매개변수 이름 끝에 ?
를 붙이면 된다.
let greeting = (firstName: string, lastName?: string): string => {
return `hello, ${firstName} ${lastName}`;
}
// 정상 작동
greeting('coding');
// 정상 작동
greeting('coding', 'kim');
// 에러(매개변수 과잉)
greeting('coding', 'kim', 'hacker');
둘 이상의 타입을 합쳐서 만들어진 새로운 타입이다. (feat. 합집합)
|
연산자를 사용한다.number | string
: 숫자 또는 문자열 타입function printValue(value: number|string): void {
if (typeof value === "number") {
console.log(`The value is a number: ${value}`);
} else {
console.log(`The value is a string: ${value}`);
}
}
printValue(10);
printValue("hello");
🔴 단, 유니온 타입 사용 시 주의할 점이 있다.
두 객체를 유니온 타입으로 묶는다면, 유니온에 있는 모든 타입에 공통적인 멤버들에만 접근할 수 있다.
interface Developer {
name: string;
skill: string;
}
interface Person {
name: string;
age: number;
}
위와 같이 두 인터페이스 Developer
, Person
을 정의했다면,
두 객체가 동시에 갖고 있는 name
프로퍼티ㅔ만 접근할 수 있는 것이다.
만약 다른(공통되지 않은) 프로퍼티에 접근하고 싶다면 Type Guard 를 사용해야 한다.
타입 가드(Type Guard)란?
TypeScript에서 타입을 보호하기 위해 특정 코드 블록에서 타입의 범위를 제한해 해당 코드 블록 안에서 타입 안정성을 보장해주는 것.
function askSomeone(someone: Developer | Person) {
if ('skill' in someone) // Type Guard
console.log(someone.skill);
if ('age' in someone) // Type Guard
console.log(someone.age);
}
type Person = {
name: string;
age: string | number;
};
function printAge(person: Person) {
let age;
if (typeof person.age === 'number' || typeof person.age === 'string') {
age = person.age.toString();
}
console.log(`${person.name}의 나이는 ${age}살 입니다.`);
}
const kimcoding = {
name: '김코딩',
age: 30,
};
const parhacker = {
name: '박해커',
age: '서른',
};
printAge(kimcoding);
printAge(parhacker);
둘 이상의 타입을 결합하여 새로운 타입을 만드는 방법
&
연산자를 사용한다.
string & number & boolean
: 변수는 string
, number
, boolean
타입을 전부 받을 수 있다.
인터섹션으로 타입을 연결해 하나의 단일 타입으로 표현한다.
따라서 타입 가드가 필요 없다.
interface Developer {
name: string;
skill: string;
}
interface Person {
name: string;
age: number;
}
function askSomeone(someone: Developer & Person) {
console.log(someone.age); // 가능
console.log(someone.name); // 가능
console.log(someone.skill); // 가능
}
interface User {
name: string;
email: string;
}
interface Admin {
name: string;
isAdmin: boolean;
}
function sendEmail(user: User & Admin) {
console.log(`안녕하세요, ${user.name}!`);
if (user.isAdmin) {
console.log(
`
권한이 admin이시군요.
이메일은 ${user.email} 입니다.
`
);
} else {
console.log(
`
권한이 user이시군요.
이메일은 ${user.email} 입니다.
`
);
}
}
const kimcoding = {
name: '김코딩',
email: 'kimcoding@codestates.com',
isAdmin: false,
};
const parkhacker = {
name: '박해커',
email: 'parkhacker@codestates.com',
isAdmin: true,
};
sendEmail(kimcoding);
sendEmail(parkhacker);
🔵 유니온(Union) 연산자와의 차이점
두 타입의 새로운 교집합을 만들어 내는 것이기 때문에, 전달 인자를 전달할 때 모든 프로퍼티를 전부 보내줘야 한다.
반면 유니온 타입은 타입가드를 해주는 대신 전달인자를 선택하여 전달할 수 있다.
// Union
function askSomeone(someone: Developer | Person) {
...
}
askSomeone({name: '김코딩', skill: '웹 개발'});
askSomeone({name: '김코딩', age: 20}); // 선택해서 전달할 수 있다.
// Interaction
function askSomeone2(someone: Developer & Person) {
...
}
askSomeone2({name: '김코딩', skill: '웹 개발', age:20}); // 무조건 다 전달해야 한다.