TypeScript(타입스크립트)란
TypeScript는 마이크로소프트에서 개발한 JavaScript의 상위 집합(Superset) 언어입니다.
JavaScript에 정적타입 검사와 클래스 기반 객체 지향 프로그래밍 등의 기능을 추가하여 개발된 언어로, JavaScript가 발전하면서 생긴 단점을 보완하기 위해 등장하게 되었습니다.
TypeScript의 등장 배경
JavaScript는 처음에는 브라우저에서만 동작하는 스크립팅 언어로 만들어졌었습니다.
시간이 점점 흐르고, JavaScript로 웹 애플리케이션의 상호작용이 증가하면서, 웹 애플리케이션이 필요로 하는 JavaScript 코드의 양이 폭발적으로 늘어나게 되었습니다.
이로 인해 JavaScript의 한계가 부각되기 시작했습니다. JavaScript는 동적 타입이 결정되어 유연하고, 다양한 라이브러리와 프레임워크를 사용할 수 있는 장점이 있지만, 타입의 명시성이 부족하다는 단점이 있습니다.
타입의 명시성이 부족하게 되면 예상치 못한 결과를 초래할 수 있습니다.
TypeScript를 사용했을 시 장점
TypeScript는 정적타입 검사 기능을 제공하며, 코드의 가독성과 유지 보수성을 높여줍니다. 이를 통해 개발자는 런타임 에러를 최소화하고, 코드 작성 시간을 단축하며, 협업 시 코드의 가독성을 높일 수 있습니다.
또한 TypeScript는 ES6의 문법을 포함한 최신 JavaScript 문법을 지원하며, 인터페이스(Interface), 제네릭(Generic), 데코레이터(Decorators) 등의 기능을 제공하여 객체 지향 프로그래밍을 보다 쉽게 할 수 있도록 도와줍니다.
TypeScript의 타입
Boolean
let isShow: boolean = true;
let isDone: boolean = false;
Number
let number1: number = 5;
let number2: number = 0.7;
String
let firstName: string = "coding";
let lastName: string = 'kim';
let longString: string = `Kimcoding is a developer.
He is 20 years old.`
Array
//첫 번째 방법
let items: string[] = ["apple", "banana", "grape"];
//두 번째 방법
let numberList: Array<number> = [4, 7, 100];
Tuple
let user: [string, number, boolean] = ["kimcoding", 20, true];
Object
let user: {name: string, age: number} = {
name: "kimcoding",
age: 20
}
Any
let obj: object = {};
//에러가 납니다.
obj = "hello";
let maybe: any = 4;
//정상적으로 동작합니다.
maybe = true;
let list: any[] = [1, true, "free"];
//any로 다루고 있기 때문에 index 1번째 요소가 boolean 타입이지만 number 타입으로 재할당할 수 있습니다.
list[1] = 100;
TypeScript의 함수
JavaScript
//named function
function add(x, y){
return x + y;
}
//arrow function
let add = (x, y) => {
return x + y;
}
TypeScript
//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");
}
TypeScript는 매개변수의 개수에 맞춰 전달인자를 전달해야 합니다.
undefined를 전달했을 때 할당될 매개변수의 값을 정해놓을 수도 있습니다.
이는 JavaScript에서의 default parameter와 같은 동작을 합니다.
let greeting = (firstName: string, lastName: string ="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}`;
}
TypeScript의 연산자 활용 타입
TypeScript는 연산자를 이용해 타입을 정할 수 있습니다.
JavaScript에서도 보았던 || (OR) 연산자나 && (AND)와 같은 연산자를 이용하여 만들 수 있습니다.
| 연산자를 이용한 타입을 유니온(Union) 타입이라고 하며, & 연산자를 이용한 타입은 인터섹션(Intersection) 타입이라고 부릅니다
TypeScript의 열거형(Enum)
TypeScript의 열거형(Enum)은 특정 값의 집합을 정의할 때 사용됩니다. JavaScript에서는 기본적으로 열거형을 지원하지 않지만, TypeScript에서는 문자형 열거형과 숫자형 열거형을 지원합니다.
enum Color {
Red,
Green,
Blue,
}
숫자형 열거형(Enum)
열거형은 디폴트 값으로 숫자형을 사용하며, 각 값은 자동으로 0부터 1씩 증가합니다.
수동으로 값을 지정할 수도 있고 산술 연산을 수행할 수도 있습니다.
enum Color {
Red = 1,
Green = 2,
Blue = 4,
}
let c: Color = Color.Green;
let greenValue: number = Color.Green;
let blueValue: number = Color.Blue;
console.log(c); // 출력: 2
console.log(greenValue); // 출력: 2
console.log(blueValue); // 출력: 4
열거형은 코드를 더욱 가독성 높게 만들어주고, 오타와 같은 실수를 방지해 줍니다.
문자형 열거형(Enum)
문자형 열거형은 숫자형 열거형과 개념적으로 거의 비슷합니다.
문자형 열거형은 값을 전부 다 특정 문자 또는 다른 열거형 값으로 초기화해야 합니다.
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
let myDirection: Direction = Direction.Up;
console.log(myDirection); // 출력: "UP"
숫자형 열거형과는 다르게 auto-incrementing이 없습니다. 대신 디버깅을 할 때 숫자형 열거형의 값은 가끔 불명확하게 나올때가 있지만 문자형 열거형은 항상 명확한 값이 나와 읽기 편합니다.
문자열 기반의 열거형은 주로 외부에서 가져온 값을 다루기 위해서 사용됩니다.
예를 들어, HTTP 요청 방식을 나타내는 열거형을 정의할 수 있습니다.
enum HttpMethod {
Get = "GET",
Post = "POST",
Put = "PUT",
Delete = "DELETE",
}
function makeRequest(url: string, method: HttpMethod) {
// ...
}
makeRequest("/api/data", HttpMethod.Post);
역 매핑(Reverse mappings)
역 매핑은 숫자형 열거형에만 존재하는 특징입니다.
키(key)로 값(value)을 얻을 수 있고, 값으로 키를 얻을 수도 있습니다.
enum Enum {
A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
TypeScript의 인터페이스(Interface)
TypeScript에서 인터페이스(Interface)는 일반적으로 타입 체크를 위해 사용이 됩니다. 인터페이스는 변수, 함수, 클래스에 사용할 수 있으며, 인터페이스에 선언된 프로퍼티 또는 메서드의 구현을 강제하여 일관성을 유지하도록 합니다.
변수와 인터페이스
interface User {
name: string;
age: number;
}
// 정상적으로 선언됩니다.
const user: User = {
name: "anna",
age: 20
}
// 프로퍼티의 순서를 지키지 않아도 정상적으로 선언됩니다.
const user: User = {
age: 20,
name: "anna"
}
// 정의된 프로퍼티보다 적게 작성했기 때문에 에러가 납니다.
const user: User = {
name: "anna"
}
// 정의된 프로퍼티보다 많이 작성했기 때문에 에러가 납니다.
const user: User = {
name: "anna",
age: 20,
job: "developer"
}
인터페이스를 만들 때 예약어를 작성하고 이름을 대문자로 시작합니다(네이밍 컨벤션).
? 연산자를 사용하여 선택적 프로퍼티를 작성할 수도 있습니다.
interface User {
name: string;
age?: number;
}
// 정상적으로 선언됩니다.
const user: User = {
name: "anna"
}
함수와 인터페이스
interface User {
name: string;
age: number;
job: string;
}
interface Greeting {
(user: User, greeting: string): string;
}
const greet: Greeting = (user, greeting) => {
return `${greeting}, ${user.name}! Your job : ${user.job}.`;
}
const user: User = {
name: "anna",
age: 30,
job: "developer"
};
const message = greet(user, "Hi")
User 인터페이스 외에도 Greeting 인터페이스를 추가로 작성하여 함수 타입을 정의했습니다.
Greeting 인터페이스는 User 타입과 문자열 타입을 매개변수로 받아 문자열 타입을 반환합니다.
greet 함수는 Greeting 을 사용하여 구현되었으며, user 객체와 문자열 "Hi" 를 전달인자로 전달받아 문자열을 반환합니다.
Greeting 인터페이스에서 이미 greet 의 매개 변수인 user 와 greeting 의 타입과 반환 타입이 작성되어 있기 때문에, greet 함수는 string 타입을 반환한다고 명시하지 않아도 되고, 매개 변수의 타입 또한 작성하지 않아도 됩니다.
클래스와 인터페이스
interface Calculator {
add(x: number, y: number): number;
substract(x: number, y: number): number;
}
class SimpleCalculator implements Calculator {
add(x: number, y:number) {
return x + y;
}
substract(x: number, y: number) {
return x - y;
}
}
const caculator = new SimpleCalculator();
위 코드에서 Calculator 인터페이스는 add 와 substract 메서드를 정의하고 있고, SimpleCalculator 클래스는 Calculator 인터페이스를 사용하여 작성되었습니다. Calculator 인터페이스를 사용하고 있기 때문에 SimpleCalculator 클래스 내에서 Calculator 인터페이스 내에 정의된 두 메서드를 반드시 작성해야 합니다.
클래스를 구현할 때 인터페이스에서 정의된 함수나 메서드의 매개변수 타입과 반환 값과 일치하도록 구현해야 하므로, 클래스 내부에서 해당 메서드의 매개변수 타입을 한번 더 명시해 주지 않으면 컴파일 에러가 발생하게 됩니다.
인터페이스와 상속
extends 라는 키워드를 사용하여 기존에 존재하던 인터페이스를 상속해 확장이 가능합니다.
이렇게 하면 기존에 존재하던 인터페이스의 프로퍼티를 다른 인터페이스에 복사하는 것을 가능하게 해 주며, 인터페이스의 재사용성을 높여줍니다.
interface Person {
name: string;
age: number;
}
interface Developer extends Person {
language: string;
}
const person: Developer = {
language: "TypeScript",
age: 20,
name: "Anna",
}
여러 인터페이스를 상속받아 확장할 수도 있습니다.
interface FoodStuff {
name: string;
}
interface FoodAmount {
amount: number;
}
interface FoodFreshness extends FoodStuff, FoodAmount {
isFreshed: boolean;
}
const food = {} as FoodFreshness;
food.name = "egg";
food.amount = 2;
food.isFreshed = true;
TypeScript의 타입 별칭(Type Aliases)
타입 별칭(Type Aliases)은 타입의 새로운 이름을 만드는 것입니다. 이는 새로운 이름으로 기존의 타입을 참조하는 것을 의미합니다.
type MyString = string;
let str1: string = 'hello!';
// string 타입처럼 사용할 수 있습니다.
let str2: MyString = 'hello world!';
string 이라는 타입이 존재하고 있는데, 이에 myString 이라는 새로운 이름을 부여했습니다.
여기서 myString 과 string 은 동일한 의미를 갖게 됩니다.
즉, 타입을 정의할 수 있는 모든 곳에는 타입 별칭을 쓸 수 있습니다.
이런 방식으로 타입 별칭을 사용하면 코드를 더 간결하고 가독성 좋게 만들 수 있습니다.
또한 복잡한 타입을 간략하게 표현하고, 타입 정의를 재사용하는 등 가독성을 높일 수 있습니다.
타입 별칭으로 만들어진 타입을 참조할 시에는 인터페이스와 마찬가지로 내부에 정의된 프로퍼티를 전부 참조해야만 합니다. 또한 별칭으로 만들어진 타입 내부에 정의된 프로퍼티 외에 다른 프로퍼티를 더 작성하게 되면 그 또한 컴파일 에러가 납니다.
인터페이스 vs 타입 별칭
작성된 코드에서 타입은 마우스를 올리면 내부에 어떤 프로퍼티들이 정의되어 있는지 보입니다.
그러나 인터페이스는 내부 프로퍼티들이 보이지 않습니다.
타입 별칭은 타입에 새로운 이름을 부여하는 것에서 그치기 때문에 확장이 되지 않습니다.
그러나 인터페이스는 확장이 가능합니다.
TypeScript의 타입 추론(Type Inference)
타입 추론(Type Inference)은 변수나 함수의 타입을 선언하지 않아도 TypeScript가 자동으로 유추하는 기능입니다.
타입 추론(Type Inference)의 기본
let isNumber = 123;
변수 isNumber 를 선언하고, 숫자 123 을 할당했습니다. 이 경우, 타입스크립트는 isNumber 의 타입을 자동적으로 숫자(Number)로 추론합니다.
최적 공통 타입(Best common type)
TypeScript는 여러 표현식에서 타입 추론이 발생할 때, 해당 표현식의 타입을 사용하여 "최적 공통 타입"을 계산합니다.
let x = [0, 1, null];
x 타입을 추론하려면 각 배열 요소의 타입을 고려해야 합니다. 여기서 배열의 타입으로 고를 수 있는 두 가지 후보가 있습니다(number, null). 최적 공통 타입 알고리즘은 각 후보의 타입을 고려하여, 모든 후보의 타입을 포함할 수 있는 타입을 선택합니다.
문맥상의 타이핑(Contextula Typing)
타입스크립트에서 타입을 추론하는 또 하나의 방식은 바로 문맥상으로 타입을 결정하는 것입니다. 이 문맥상의 타이핑(타입 결정)은 코드의 위치(문맥)를 기준으로 일어납니다.
function add(a, b) {
return a + b;
}
add 함수는 두 개의 매개변수를 받아 더한 값을 반환합니다. 하지만 매개변수의 타입이 명시되어 있지 않습니다. 이 경우, 타입스크립트는 매개변수 a 와 b 의 타입을 자동으로 추론했습니다.
만약 매개변수 a 와 b 가 모두 Number 타입이라면, add 함수의 반환 값도 Number 타입으로 추론됩니다.
타입 추론의 장점, 단점
장점
- 코드의 가독성 향상: 명시적으로 타입을 지정하지 않아도 코드에서 변수의 타입을 알 수 있기 때문입니다.
- 개발 생산성 향상: 명시적으로 타입을 지정하지 않아도 TypeScript가 자동으로 타입을 추론하기 때문입니다.
- 오류 발견 용이성: TypeScript는 변수나 함수의 타입을 추론하여 타입 검사를 수행하기 때문입니다.
단점
- 타입 추론이 잘못될 경우 코드 오류 발생
- 타입 추론만으로는 부족한 경우, 명시적인 타입 지정이 필요
TypeScript의 클래스(Class)
TypeScript의 클래스는 JavaScript의 클래스와 비슷하지만 몇 가지 추가된 기능이 있습니다. 예를 들어, TypeScript에서는 클래스의 속성과 메서드에 대한 타입을 명시할 수 있습니다.
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet(): void {
console.log(`안녕하세요, 제 이름은 ${this.name}이고, ${this.age}살 입니다.`);
}
}
name 속성과 age 속성은 문자열과 숫자 타입으로 정의되어 있습니다. 약간의 차이점은 TypeScript에서 클래스를 정의할 때, constructor 를 이용하여 초기화하는 멤버들은 전부 상단에서 정의를 해줘야 한다는 것입니다. 또한 constructor 내 인자로 받을 때도 정확히 타입을 명시해 줘야 합니다.
const person = new Person('Alice', 30);
person.greet(); // "안녕하세요, 제 이름은 Alice이고, 30살 입니다."
TypeScript를 사용하여 객체를 생성할 때도 위와 같이 JavaScript와 비슷한 방식으로 할 수 있습니다.
클래스와 상속(Inheritance)
class Animal {
move(distanceInMeters: number): void {
console.log(`${distanceInMeters}m 이동했습니다.`);
}
}
class Dog extends Animal {
speak(): void {
console.log("멍멍!");
}
}
const dog = new Dog();
dog.move(10);
dog.speak();
TypeScript의 클래스는 인터페이스와 마찬가지로 기존에 존재하던 클래스를 상속받아 확장하여 새로운 클래스를 만들 수 있습니다. 이때도 extends 키워드를 사용합니다.
Animal 이라는 클래스를 Dog 라는 클래스가 상속받고 있습니다.
Dog 클래스는 Animal 클래스로부터 프로퍼티와 메서드를 상속받으며, Dog 클래스는 파생 클래스라고도 불리며, 하위클래스(subclasses)라고도 불립니다.
Animal 클래스는 기초 클래스, 상위클래스(superclasses)라고 불립니다.
public, private 키워드
기본적으로 클래스 내에 선언된 멤버는 외부로 공개되는 것이 디폴트 값입니다.
그러나 공개된다고 명시적으로도 표시해 줄 수 있습니다. 이때 public 키워드를 사용하면 됩니다.
또한 외부에 드러내지 않을 멤버가 있다면 private 키워드로 명시해 주면 됩니다.
class Person {
public name: string;
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet(): void {
console.log(`안녕하세요, 제 이름은 ${this.name}이고, ${this.age}살 입니다.`);
}
}
readonly 키워드
readonly 키워드를 사용하여 프로퍼티를 읽기 전용으로 만들 수 있습니다.
읽기 전용 프로퍼티들은 선언 또는 생성자에서 초기화해야 합니다.
class Mydog {
readonly name: string;
constructor(theName: string) {
this.name = theName;
}
}
let spooky = new Mydog("스푸키");
spooky.name = "멋진 스푸키"; // 에러
이런 식으로 변경되면 안 될 값을 readonly 로 명시하여 보호할 수 있습니다.