🔥 학습목표
- TypeScript의 열거형, 인터페이스, 타입 별칭, 클래스에 대해 학습한다.
- 위 4가지 문법을 적용해 JavaScript 코드를 TypeScript로 포팅할 수 있다.
특정 값의 집합을 정의
열거형의 형태
enum Color {
Red,
Green,
Blue,
}
열거형은 디폴트 값으로 숫자형을 사용한다.
각 값은 자동으로 0부터 시작하여 1씩 증가한다.
숫자를 직접 정의할 수도 있다.
enum Color {
Red = 1,
Green = 2,
Blue = 4,
위 예시는 각각 1
, 2
, 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
console.log(c + greenValue); // 출력: 4
열거형은 일반적으로 상수값을 대신하여 사용된다.
코드의 가독성을 높여주고 오타를 방지한다.
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
let myDirection: Direction = Direction.Up;
console.log(myDirection); // 출력: "UP"
auto-incrementing
이 없어 증가하거나 값이 변하지 않는다. 늘 명확한 값을 가져온다.문자형은 주로 외부에서 가져온 값을 TypeScript에서 다룰 때 사용한다.
예를 들어, HTTP 요청 방식을 나타내는 열거형은 아래와 같이 정의할 수 있다.
enum HttpMethod {
Get = "GET",
Post = "POST",
Put = "PUT",
Delete = "DELETE",
}
makeRequest("/api/data", HttpMethod.Post);
숫자형 열거형에만 존재하는 특징이다.
키(key
) 값으로 값(value
)을 얻어낼 수 있고
값(value
)으로 키(key
)를 얻을 수도 있다.
enum Enum {
A
}
let a = Enum.A; // 0 (키로 값을 얻음)
let nameOfA = Enum[a]; // "A" (값으로 키를 얻음)
그러나. 이렇게 기껏 enum에 대해 알아봤더니 enum에 대한 여러가지 의견들이 존재했다.
오.. 하나하나 읽어보면 좋을 것 같다.
🎁 enum 대신 union type
🎁 enum을 사용하지 않는 게 좋은 유
타입 체크를 하는 데 용이하다. 변수, 함수, 클래스에 사용할 수 있으며, 인터페이스에 선언된 프로퍼티 또는 메서드의 구현을 강제하여 일관성을 유지한다.
JS는 인터페이스를 따로 지원하지 않는다.
예약어 interface
를 사용하여 생성할 수 있다.
TypeScript에서 인터페이스는 객체(Object)의 구조를 정의하기 위해 주로 사용된다.
interface User {
name: string;
age: number;
}
const user: User = {
name: "daham",
age: 22
}
프로퍼티의 순서를 지키지 않아도 정상적으로 작동한다.
단 정의된 프로퍼티보다 적게 작성하면 에러가 난다.
단 정의된 프로퍼티보다 많이 작성하면 에러가 난다.
인터페이스 안의 모든 프로퍼티가 필요하지 않을 때
?
연산자를 사용하여 선택적 프로퍼티를 작성한다.
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}.`;
}
Greeting
인터페이스에서 이미 매개변수 user
와 greeting
의 타입 및 반환 타입을 작성했으므로 greet
함수에는 명시하지 않아도 된다.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();
클래스를 구현할 때 인터페이스에서 정의된 함수, 매개변수 타입, 반환 값이 일치해야 한다.
따라서 클래스 내부에서 해당 메서드의 매개변수 타입을 다시 한 번 명시해주지 않으면 에러가 발생한다.
실습
// Todo 인터페이스
interface Todo {
id: number;
content: string;
isDone: boolean;
}
// Todo 인터페이스를 타입으로 받는 todos
let todos: Todo[] = [];
// let todos: Array<Todo> = [];
// Todo 인터페이스를 타입으로 받는 addTodo
function addTodo(todo: Todo): void {
todos = [...todos, todo];
}
// Todo 인터페이스를 타입으로 받는 newTodo
const newTodo: Todo = {
id: 1,
content: 'learn typescript',
isDone: false,
};
addTodo(newTodo);
console.log(todos); // [ { id: 1, content: 'learn typescript', isDone: false } ]
JavaScript는 클래스를 확장할 때 extends
키워드를 사용한다.
TypeScript의 인터페이스도 extends
키워드를 사용하여 확장이 가능하다.
기존에 존재하던 인터페이스의 프로퍼티를 다른 인터페이스에 복사하는 것이 가능하다.
interface Person {
name: string;
age: number;
}
interface Developer extends Person {
language: string;
}
const person: Developer = {
language: "TypeScript",
age: 20,
name: "DaHam",
}
Developer
인터페이스는 Person
인터페이스를 상속하고 있으므로 Person
내부의 프로퍼티를 그대로 받아올 수 있다.
여러 개의 인터페이스를 상속 받는 방법도 있다.
interface FoodStuff {
name: string;
}
interface FoodAmount {
amount: number;
}
interface FoodFreshness extends FoodStuff, FoodAmount {
isFreshed: boolean;
}
실습 1
/* 코드를 작성한 뒤
1. tsc src/index.ts
2. node src/index.js
순으로 터미널에 입력하여 결과를 확인해 주세요.
*/
/* 실습 1 */
interface User {
name: string;
age: number;
}
//Student 인터페이스
interface Student extends User {
grade: number;
}
//Student 인터페이스를 받는 kimcoding
const kimcoding: Student = {
name: '김코딩',
age: 20,
grade: 1,
};
console.log(kimcoding); // { name: '김코딩', age: 20, grade: 1 }
실습 2
interface Color {
name: string;
brightness: number;
}
interface ClothesType {
kind: string;
length: number;
}
const Season = {
SPRING: '봄',
SUMMER: '여름',
AUTHUMN: '가을',
WINTER: '겨울',
} as const; // readonly 로 바뀜.
type Season = typeof Season[keyof typeof Season];
//Closet 인터페이스
interface Closet extends Color, ClothesType {
season: Season;
}
//Closet 인터페이스를 받는 skirt
const skirt: Closet = {
name: 'yellow',
brightness: 0,
kind: 'skirt',
length: 5,
season: Season.SUMMER,
};
console.log(skirt);
/*
{
name: 'yellow',
brightness: 0,
kind: 'skirt',
length: 5,
season: 'summer'
}
*/
타입의 새로운 이름을 만드는 것
JavaScript에서는 타입 별칭을 지원하지 않는다.
TypeScript에서는 타입의 새로운 이름을 만들 때 키워드 type
을 사용한다.
type MyString = string;
let str1: string = 'hello!';
let str2: MyString = 'hello world!';
위에서 string
타입에 MyString
이라는 별칭을 지정했다. 그렇게 하면 MyString
도 string
타입처럼 사용할 수 있다.
type Person = {
id: number;
name: string;
email: string;
}
interface Commentary {
id: number;
content: string;
user: Person; // Person 타입을 참조한다.
}
let comment1: Commentary = {
id: 1,
content: "뭐예요?",
user: {
id: 1,
name: "김코딩",
email: "kimcoding@codestates.com",
},
}
Person
타입 별칭을 참조하여 Commentary
인터페이스를 정의하였다.
Commentary
인터페이스를 참조하여 객체를 만들 때,
Commentary
인터페이스 내부에 존재하는 프로퍼티를 작성하지 않으면 에러가 난다.Commentary
인터페이스 내부에 존재하지 않는 프로퍼티를 작성하면 에러가 난다.Person
타입 내부에 존재하는 프로퍼티를 적지 않으면 에러가 난다.Person
타입 내부에 존재하지 않는 프로퍼티를 적으면 에러가 난다.타입 별칭
hover
하면 내부에 어떤 프로퍼티들이 정의되어 있는지 보여준다.extends
로 확장이 불가능하다.type
은 unique 해야 한다.인터페이스
hover
하면 내부에 어떤 프로퍼티들이 정의되어 있는지 보여주지 않는다.=> 인터페이스는 기존 인터페이스 및 타입 별칭 모두 다 상속할 수 있기 때문에, 유연한 코드 작성을 위해서는 인터페이스로 만들어 필요할 때마다 확장하는 게 좋다.
JavaScript, TypeScript 모두 객체지향 프로그래밍을 지원하며 클래스(class)를 사용할 수 있다.
단, 몇가지 차이점이 있다.
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`안녕하세요, 제 이름은 ${this.name}이고, ${this.age}살 입니다.`);
}
}
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}살 입니다.`);
}
}
TypeScript에서는 클래스의 속성과 메서드에 대한 타입을 명시한다.
constructor
를 이용하여 멤버들을 초기화 하기 전에 전부 상단에서 타입 정의를 해줘야 한다.
constructor
내에서 인자로 받을 때도 정확히 타입을 명시한다.
TypeScript 클래스는 인터페이스와 마찬가지로 기존에 존재하던 클래스를 상속 받아 확장할 수 있다.
마찬가지로 extends
키워드를 사용한다.
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();
그 외
public
, private
을 지정해줄 수 있다.
readonly
키워드를 사용하여 프로퍼티를 읽기 전용으로 만들 수 있다.
class Mydog {
readonly name: string;
constructor(theName: string) {
this.name = theName;
}
}
실습 1
class Counter {
value: number;
constructor() {
this.value = 0;
}
increase() {
this.value++;
}
decrease() {
this.value--;
}
getValue(): number {
return this.value;
}
}
let counter1 = new Counter();
counter1.increase();
console.log(counter1.getValue()); // 1
실습 2
class Animal {
name: string;
constructor(theName) {
this.name = theName;
}
speak(sound = '왕왕!'): void {
console.log(`${this.name}(은/는) ${sound}하고 웁니다.`);
}
}
class Mouse extends Animal {
constructor(name) {
super(name);
}
speak(sound = '찍찍'): void {
super.speak(sound);
}
}
class Cat extends Animal {
constructor(name) {
super(name);
}
speak(sound = '야옹'): void {
super.speak(sound);
}
}
let jerry = new Mouse('제리');
let tom = new Cat('톰');
jerry.speak(); // 제리(은/는) 찍찍하고 웁니다.
tom.speak('냥냥'); // 톰(은/는) 냥냥하고 웁니다.
함수나 클래스를 작성할 때, 사용될 데이터의 타입을 미리 지정하지 않고, 이후 함수나 클래스를 호출할 때 인자로 전달된 데이터의 타입에 따라 자동으로 타입을 추론한다.
함수 printLog
는 파라미터로 string
타입의 text
를 받고, string
타입의 반환값을 만든다.
function printLog(text: string): string {
return text;
}
printLog('hello'); // 정상
printLog(123); //에러
당연하겠지만 인자로 문자열이 아닌 다른 값을 전달하게 되면 에러가 발생한다.
하지만 다른 인자를 받아도 잘 수행되는 범용적인 함수를 만들 수는 없을까?
string
타입을 인자로 받는 함수와 number
타입을 인자로 받는 함수를 2개 생성한다.
=> 타입을 제외하곤 코드가 중복되며 가독성, 유지보수성까지 나빠진다.
|
연산자를 사용해 유니온 타입으로 선언한다.
=> 결국 string | number
일 경우 저 두 개의 타입 밖에 접근을 못 한다.
any
타입을 사용한다.
=> 어떤 타입이든 받을 수 있지만 실제로 함수가 반환할 때 어떤 타입인지는 알 수 없게 된다.
따라서 그 해결책으로 제네릭을 사용하게 된다.
function printLog<T>(text: T): T {
return text;
}
제네릭 코드를 자세히 살펴보자.
function printLog<T>(text: T): T {
return text;
}
printLog
함수에 T
라는 타입 변수를 추가한다.T
는 유저가 준 파라미터의 타입을 캡처하고, 이 정보를 나중에 사용한다.제네릭 함수는 다음과 같이 호출한다.
const str = printLog<string>('hello');
T
를 string
타입으로 명시해주고 주변을 <>
로 감싸준다.
any
와 달리 타입을 추론할 수 있다.
const str = printLog('hello');
또는 타입 추론 기능을 활용해서 타입 기입을 생략할 수도 있다. 단, 타입이 복잡해지면 컴파일러가 타입을 유추할 수 없기 때문에 이 방법은 사용할 수 없다.
위 캡처 화면과 같이 타입 변수 T
자리에 사용자 입력값이 치환되는 걸 볼 수 있다.
인터페이스에 제네릭을 사용하면 달라지는 타입마다 인터페이스를 여러 개 만들지 않고도 재사용 할 수 있다.
interface Item<T> {
name: T;
stock: number;
selected: boolean;
}
아래와 같이 여러 개의 객체를 만들 수 있게 된다.
const obj: Item<string> = {
name: "T-shirts",
stock: 2,
selected: false
};
const obj: Item<number> = {
name: 2044512,
stock: 2,
selected: false
};
name
자리에 문자열도 들어가고, 숫자도 들어갈 수 있다.
제네릭을 사용하는 TypeScript에서 클래스를 생성할 때, 생성자 함수에 클래스 타입을 참조해야 한다.
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
제네릭 함수를 만들 때, 컴파일러가 함수 본문에 제네릭 타입화 된 매개변수를 쓰도록 강요한다.
function printLog<T>(text: T): T {
console.log(text.length);
return text;
}
어떤 타입이 들어올지 모르는 매개변수 text
는 .length
에 접근 가능한지 추론을 할 수 없다.
이때 "제네릭에" "타입"을 준다.
function printLog<T>(text: T[]): T[] {
console.log(text.length);
return text;
}
제네릭 함수는 T
라는 변수 타입을 받고
인자값으로 배열 형태의 T
를 받는다.
따라서 제네릭이 배열 타입이기 때문에 .length
에 접근할 수 있게 되는 것이다.
더 명시적으로 작성하기 위해선 아래와 같이 적을 수 있다.
function printLog<T>(text: Array<T>): Array<T> {
console.log(text.length);
return text;
}
제네릭 타입 변수 외 제네릭 함수에 어떤 타입이 들어올 것인지 어느정도 힌트를 줄 수 있다.
interface TextLength {
length: number;
}
function printLog<T extends TextLength>(text: T): T {
console.log(text.length);
return text;
}
const Test = {
length: 1,
name: 'daham',
};
printLog(Test); // 출력: 1
length
속성을 가지고 있는 TextLength
인터페이스를 만든 뒤,
T
에 extends
지시자를 작성하게 되면 타입에 대해 강제적이지 않으면서도 length
에 대해 동작하는 인자만 넘겨받을 수 있다.
다른 제약 방법으론 keyof
가 있다.
interface Item<T> {
name: T;
stock: number;
selected: boolean;
}
function printLog<T extends keyof Item>(text: T): T {
return text;
}
printLog('name'); //정상
pirntLog('key'); //에러
T
는 Item
인터페이스가 가지고 있는 속성에 대해서만 인자로 받는다.