본문 바로가기

[TS] 열거형(Enum) / 인터페이스 / 클래스 / 상속

[TS] 열거형(Enum) / 인터페이스 / 클래스 / 상속

열거형(Enum)

TypeScript의 열거형(Enum)은 특정 값의 집합을 정의할 때 사용된다.

  •  JavaScript에서는 기본적으로 열거형을 지원하지 않지만, 열거형을 흉내 낼 수는 있다.
  •  TypeScript에서는 문자형 열거형과 숫자형 열거형을 지원한다.
  • TypeScript에서 열거형은 다음과 같은 형태로 정의할 수 있다.
    • 위 예제에서는 Color라는 열거형을 정의하고 있다.
    • 열거형의 값은 Red, Green, Blue 세 개이다.
enum Color {
  Red,
  Green,
  Blue,
}

 

숫자형 열거형(Enum)

열거형은 숫자형과 문자열형, 혹은 이 둘의 조합으로 정의될 수 있다.

  • 디폴트 값으로 숫자형을 사용하며, 각 값은 자동으로 0부터 시작하여 1씩 증가한다. (auto-incrementing)
  • 그러나 다음과 같이 수동으로 값을 지정할 수도 있다.
enum Color {
  Red = 1,
  Green = 2,
  Blue = 4,
}

 

열거형의 값에 대해 산술 연산을 수행할 수도 있다.

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)

문자형 열거형은 앞에서 살펴본 숫자형 열거형과 개념적으로는 거의 비슷하다.

  • 문자형 열거형은 열거형의 값을 전부 다 특정 문자 또는 다른 열거형 값으로 초기화해야 한다.
    • 즉, 문자형 열거형에는 숫자형 열거형과는 다르게 auto-incrementing이 없다.
    • 대신 디버깅을 할 때 숫자형 열거형의 값은 가끔 불명확하게 나올 때가 있지만 문자형 열거형은 항상 명확한 값이 나와 읽기 편하다.
  • 아래 코드는 Direction이라는 문자열 기반의 열거형(Enum)을 정의하고 있다. 
    • Up, Down, Left, Right 각각에는 문자열 값이 할당되어 있다.
    • 그리고 myDirection 변수를 Direction.Up으로 초기화하고 있다.
    • 출력 결과로는 "UP"이 나온다.
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

let myDirection: Direction = Direction.Up;
console.log(myDirection); // 출력: "UP"

 

문자열 기반의 열거형은 주로 외부에서 가져온 값을 TypeScript에서 다루기 위해서 사용된다.

  • 예를 들어, HTTP 요청 방식을 나타내는 열거형을 정의할 수 있다.
  • 아래 코드에서는 HTTP 요청 방식을 나타내는 HttpMethod 열거형을 정의하고 있다. 
  • makeRequest 함수는 URL과 HTTP 요청 방식을 인자로 받는다.
  • HTTP 요청 방식을 지정할 때는 HttpMethod.Post와 같이 열거형 값을 사용한다.
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)을 얻을 수 있고 값(value)으로 키(key)를 얻을 수도 있다.
  • 아래와 같이 열거형의 키로 값을 얻을 수 있지만, 값으로도 열거형의 키를 얻을 수 있다.
enum Enum {
    A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"

 

인터페이스(Interface)

JavaScript에서는 인터페이스를 따로 지원하지 않으며, TypeScript에서만 지원된다. 

  • TypeScript에서 인터페이스(Interface)는 일반적으로 타입 체크를 위해 사용이 된다.
  • 인터페이스는 변수, 함수, 클래스에 사용할 수 있으며, 인터페이스에 선언된 프로퍼티 또는 메서드의 구현을 강제하여 일관성을 유지하도록 한다.
  • TypeScript의 예약어인 interface를 사용하여 TypeScript 인터페이스를 생성할 수 있다.

[예약어(reserved word)]

컴퓨터 프로그래밍 언어에서 이미 문법적인 용도로 사용되고 있기 때문에 식별자로 사용할 수 없는 단어를 의미한다. 예를 들어 return, import, const, let 등이 있으며, 이런 단어들은 함수의 이름이나 변수의 이름으로 사용할 수 없다.

 

변수와 인터페이스

TypeScript에서 변수를 선언할 때 인터페이스를 아래와 같이 사용할 수 있다.

  • TypeScript에서 인터페이스는 객체(Object)의 구조를 정의하기 위해 주로 사용되는 예약어이다.
  • 아래의 코드는 사용자 정보를 정의하기 위해 interface 예약어를 사용하여 User 인터페이스를 만들었다.
  •  인터페이스를 만들 때 예약어를 작성하고, 인터페이스의 이름을 대문자로 작성한다.
  • 이렇게 인터페이스의 이름을 대문자로 시작하는 것은 *네이밍 컨벤션이다.
    • 이름을 짓는 일종의 관례이다.
    • TypeScript로 개발할 때 대부분의 개발자는 인터페이스의 이름을 대문자로 시작하도록 작성한다.
    • 인터페이스는 객체의 타입을 정의하고, 객체가 대문자로 시작하는 이름을 가지는 것과 유사하기 때문에 일관성 있는 코드 작성을 위해 이러한 관례를 따르는 것이다.
  • 인터페이스 내에는 name과 age가 정의되어 있기 때문에, User 인터페이스를 사용하여 변수를 선언할 때는 반드시 정의된 프로퍼티를 전부 작성해야 한다.
  • 또한 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"
}

 

함수와 인터페이스

인터페이스를 사용하여 객체의 프로퍼티 이름과 타입을 정의하고, 함수의 매개변수 타입과 반환 타입도 정의할 수 있다.

  • 아래의 코드는 User 인터페이스 외에도 Greeting 인터페이스를 추가로 작성하여 함수 타입을 정의했다.
  • Greeting 인터페이스는 User 타입과 문자열 타입을 매개변수로 받아 문자열 타입을 반환한다.
    • greet 함수는 Greeting을 사용하여 구현되었으며, user 객체와 문자열 "hi"를 전달인자로 전달받아 문자열을 반환한다.
  • Greeting 인터페이스에서 이미 greet의 매개 변수인 user와 greeting의 타입과 반환 타입이 작성되어 있기 때문에, greet 함수는 string 타입을 반환한다고 명시하지 않아도 되며, 매개 변수의 타입 또한 작성하지 않아도 된다.
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");

console.log(message);

 

클래스와 인터페이스

클래스에서도 인터페이스를 사용할 수 있다.

  • 클래스에서는 인터페이스를 아래와 같이 사용한다.
  • 아래의 코드에서 Calculator 인터페이스는 add와 substract 메서드를 정의하고 있고, SimpleCaculator 클래스는 Calculator 인터페이스를 사용하여 작성되었다.
  • Caculator 인터페이스를 사용하고 있기 때문에 SimpleCaculator 클래스 내에는 Calculator 인터페이스 내에 정의된 두 메서드를 반드시 작성해야 한다.
  • 또한 클래스를 구현할 때 인터페이스에서 정의된 함수나 메서드의 매개변수 타입과 반환 값과 일치하도록 구현해야 하므로, 클래스 내부에서 해당 메서드의 매개변수 타입을 다시 한번 더 명시해 주지 않으면 컴파일 에러가 발생하게 된다.
interface Calculator {
  // 인터페이스의 메서드를 정의할 때 두 가지 방법을 쓸 수 있다.
  // 🟣 1. 메서드 시그니처 형태 
  // 🟡 2. 속성 형태 

	add(x: number, y: number): number;  // 🟣
    // 🟡 또는 add: (x: number, y: number) => number;
	substract(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();

console.log(caculator.add(4, 9)); //13
console.log(caculator.substract(10, 5)); //5

 

 

인터페이스와 상속

 

JavaScript에서 클래스를 확장할 때 extends라는 키워드를 사용해 기존에 존재하던 클래스를 상속해 새로운 클래스를 정의할 수 있다.

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sayHello() {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
  }
}

//Person 클래스를 extends 키워드를 사용해 상속하여 새로운 클래스인 Student를 정의했다.
class Student extends Person {
  constructor(name, age, grade) {
    super(name, age);
    this.grade = grade;
  }

  study() {
    console.log(`${this.name} is studying hard for the grade ${this.grade}.`);
  }
}

 

위의 코드를 TypeScript로 바꾸기 (아래 클래스 챕터를 보고 확인하면 이해가 더 쉽다)
더보기
class Person {
  name: string;
  age: number;

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

  sayHello(): void {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
  }
}

class Student extends Person {
  grade: string;

  constructor(name: string, age: number, grade: string) {
    super(name, age);
    this.grade = grade;
  }

  study(): void {
    console.log(`${this.name} is studying hard for the grade ${this.grade}.`);
  }
}

 

이같이 인터페이스도 extends라는 키워드를 사용하여 기존에 존재하던 인터페이스를 상속해 확장이 가능하다.

  • 이렇게 하면 기존에 존재하던 인터페이스의 프로퍼티를 다른 인터페이스에 복사하는 것을 가능하게 해 주며, 인터페이스의 재사용성을 높여준다.
  • 아래의 코드를 보면 기존에 존재하던 Person 인터페이스를 extends 키워드로 상속해 새로운 인터페이스인 Developer를 만들고, Developer 인터페이스를 사용하여 person이라는 객체를 구현했다.
  • Developer 인터페이스는 Person 인터페이스를 상속하고 있으므로, Person 내부의 프로퍼티를 그대로 받아올 수 있다.
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;
}

// 아래에 나오는 '타입단언'이다. 
// 빈 객체를 FoodFreshness 타입으로 간주하도록 TypeScript에게 알려주는 역할
// const food = <FoodFreshness>{}; 이렇게도 쓸 수 있다.
// food 변수는 FoodFreshness 타입을 가지는 것으로 간주된다. 
// 이렇게 변수의 타입 단언이 사용되면 명시적으로 :object 타입 형태로 타입을 지정하는 부분은 생략할 수 있다.
const food = {} as FoodFreshness; 

food.name = "egg";
food.amount = 2;
food.isFreshed = true;
 

TypeScript의 클래스(Class)

 

클래스는 JavaScript와 TypeScript가 동일하게 지원하는 문법이다.

  • JavaScript와 TypeScript 모두 객체 지향 프로그래밍을 지원하며 클래스(class)를 사용하여 객체를 생성할 수 있다.
  • 그러나 두 언어의 클래스는 몇 가지 차이점이 있다.

 

JavaScript에서의 클래스(Class)

JavaScript에서 클래스는 ES6(ECMAScript 2015)에서 처음 도입되었다.

  • 클래스를 사용하면 객체를 생성하고 객체의 속성과 메서드를 정의할 수 있다.
  • 예를 들어, 다음과 같은 JavaScript 클래스를 만들 수 있다.
  • 아래의 코드에서 Person 클래스는 name과 age 속성을 가지고 greet() 메서드를 정의한다.
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  greet() {
    console.log(`안녕하세요, 제 이름은 ${this.name}이고, ${this.age}살 입니다.`);
  }
}

 

이 클래스를 사용하여 객체를 생성하려면 다음과 같이 할 수 있다.

const person = new Person('Alice', 30);
person.greet(); // "안녕하세요, 제 이름은 Alice이고, 30살 입니다."

 

TypeScript에서의 클래스(Class)

TypeScript의 클래스는 JavaScript의 클래스와 비슷하지만 몇 가지 추가된 기능이 있다.

  • 예를 들어, TypeScript에서는 클래스의 속성과 메서드에 대한 타입을 명시할 수 있다.
  • 아래의 코드에서 name 속성과 age 속성은 문자열과 숫자 타입으로 정의되어 있다.
  • 약간의 차이점은 TypeScript에서 클래스를 정의할 때, constructor를 이용하여 초기화하는 멤버들은 전부 상단에서 정의를 해줘야 한다는 것이다.
  • 또한 contructor 내 인자로 받을 때도 정확히 타입을 명시해 줘야 한다.
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를 사용하여 객체를 생성할 때도 다음와 같이 JavaScript와 비슷한 방식으로 할 수 있다.

const person = new Person('Alice', 30);
person.greet(); // "안녕하세요, 제 이름은 Alice이고, 30살 입니다."

 

클래스와 상속(Inheritance)

TypeScript의 클래스(class)는 인터페이스(interface)와 마찬가지로 기존에 존재하던 클래스를 상속받아 확장하여 새로운 클래스를 만들 수 있다.

  • 이때도 extends 키워드를 사용하여 상속할 수 있다.
  • 아래의 코드에서 Animal이라는 클래스를 Dog라는 클래스가 상속받고 있다.
  • Dog 클래스는 Animal 클래스로부터 프로퍼티와 메서드를 상속받는다.
    • Dog 클래스는 파생 클래스라고도 불리며, 하위클래스(subclasses)라고도 불린다.
    • 여기서 Animal 클래스는 기초 클래스, 상위클래스(superclasses)라고 불린다.
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 키워드

기본적으로 클래스 내에 선언된 멤버는 외부로 공개되는 것이 디폴트 값이다.

  • 그러나 공개된다고 명시적으로도 표시해 줄 수 있다.
  • 이때 public 키워드를 사용하면 된다.
class Person {
  public name: string;
  public age: number;

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

  greet(): void {
    console.log(`안녕하세요, 제 이름은 ${this.name}이고, ${this.age}살 입니다.`);
  }
}

 

혹은 외부에 드러내지 않을 멤버가 있다면 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 키워드를 사용하여 프로퍼티를 읽기 전용으로 만들 수 있다.

  • 읽기 전용 프로퍼티들은 선언 또는 생성자에서 초기화해야 한다.  // 🟣 설명 (1)
  • 위의 코드는 name이 readonly로 명시되어 있기 때문에, 값을 변경할 수 없다.
  • 이런 식으로 변경되면 안 될 값을 readonly로 명시하여 보호할 수 있다.
class Mydog {
    readonly name: string;            // 🟣 설명(2)
    constructor(theName: string) {
        this.name = theName;
    }
}
let spooky = new Mydog("스푸키");
spooky.name = "멋진 스푸키"; // 에러

 

 🟣 설명 (1)  선언 또는 생성자에서 초기화?

"선언 또는 생성자에서 초기화"는 읽기 전용 프로퍼티의 값을 설정하는 방법으로, 프로퍼티를 선언 시에 초기값을 할당하거나 생성자 함수 내부에서 값을 할당하는 것을 의미한다.


선언에서 초기화하는 경우: 

프로퍼티를 선언할 때 값을 할당하는 방식이다.

  • 선언 시 값을 할당하면 초기화되어 읽기 전용 프로퍼티의 값으로 설정된다.
class MyClass {
  readonly myProperty: string = "initial value";
}



생성자에서 초기화하는 경우: 

생성자 함수 내부에서 읽기 전용 프로퍼티의 값을 할당하는 방식이다.

  • 클래스의 인스턴스를 생성할 때 생성자에서 프로퍼티 값을 설정한다.
  • 아래의 코드에서 myProperty는 생성자 내부에서 인수로 전달받은 value를 할당하여 초기화된다.
class MyClass {
  readonly myProperty: string;

  constructor(value: string) {
    this.myProperty = value;
  }
}

 

 

 🟣 설명 (2)  name과 theName

위의 코드 class Mydog에서 readonly name: string은 클래스의 멤버 속성이고,  constructor(theName: string)의 theName은 생성자 함수의 매개변수이다.

  • readonly name: string은 클래스의 멤버 속성으로 선언된 문자열 형식의 읽기 전용 속성이다. 이 속성은 클래스 내에서 변경될 수 없다.
  • constructor(theName: string)에서 theName은 매개변수로 전달된 값이며, this.name = theName;을 통해 클래스의 name 속성에 해당 값이 할당된다.
728x90
⬆︎