TypeScript (2) - 열거형, 인터페이스, 타입 별칭, 클래스, 제네릭

Kim-DaHam·2023년 5월 31일
0

JavaScript

목록 보기
17/18
post-thumbnail

🔥 학습목표

  • TypeScript의 열거형, 인터페이스, 타입 별칭, 클래스에 대해 학습한다.
  • 위 4가지 문법을 적용해 JavaScript 코드를 TypeScript로 포팅할 수 있다.



🟩 TypeScript 문법

🟣 열거형(Enum)

특정 값의 집합을 정의

  • JavaScript에서는 기본적으로 열거형을 지원하지 않는다.
  • TypeScript는 문자형 열거형 / 숫자형 열거형을 지원한다.

열거형의 형태

enum Color {
  Red,
  Green,
  Blue,
}

⬜ 숫자형 열거형(Enum)

  • 열거형은 디폴트 값으로 숫자형을 사용한다.

  • 각 값은 자동으로 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)

  • 문자형 열거형은 값을 전부 특정 문자 또는 다른 열거형 값으로 초기화해야 한다.
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);

⬜ 역 매핑(Reverse mappings)

  • 숫자형 열거형에만 존재하는 특징이다.

  • 키(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 인터페이스에서 이미 매개변수 usergreeting 의 타입 및 반환 타입을 작성했으므로 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이라는 별칭을 지정했다. 그렇게 하면 MyStringstring 타입처럼 사용할 수 있다.


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 타입 내부에 존재하지 않는 프로퍼티를 적으면 에러가 난다.

⬜ 인터페이스와 타입 별칭의 차이

  • 타입 별칭

    • VScode에서 마우스를 hover 하면 내부에 어떤 프로퍼티들이 정의되어 있는지 보여준다.
    • extends로 확장이 불가능하다.
    • type은 unique 해야 한다.
  • 인터페이스

    • VScode에서 마우스를 hover 하면 내부에 어떤 프로퍼티들이 정의되어 있는지 보여주지 않는다.
    • 확장이 가능하다.
    • 여러 번 선언 시 선언 병합이 발생한다. 선언 병합이란, 컴파일러가 같은 이름으로 선언된 두 개의 개별적인 선언을 하나의 정의로 병합하는 것을 뜻한다. 이 병합된 정의는 원래 두 선언 각각의 기능을 모두 갖게 된다.

=> 인터페이스는 기존 인터페이스 및 타입 별칭 모두 다 상속할 수 있기 때문에, 유연한 코드 작성을 위해서는 인터페이스로 만들어 필요할 때마다 확장하는 게 좋다.



🟣 클래스(Class)

  • JavaScript, TypeScript 모두 객체지향 프로그래밍을 지원하며 클래스(class)를 사용할 수 있다.

  • 단, 몇가지 차이점이 있다.

⬜ JavaScript 에서의 클래스

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

⬜ 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}살 입니다.`);
  }
}
  • 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('냥냥'); // 톰(은/는) 냥냥하고 웁니다.



🟣 제네릭(Generic)

함수나 클래스를 작성할 때, 사용될 데이터의 타입을 미리 지정하지 않고, 이후 함수나 클래스를 호출할 때 인자로 전달된 데이터의 타입에 따라 자동으로 타입을 추론한다.

⬜ 간단한 예시

함수 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');

Tstring 타입으로 명시해주고 주변을 <>로 감싸준다.

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 인터페이스를 만든 뒤,

Textends 지시자를 작성하게 되면 타입에 대해 강제적이지 않으면서도 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'); //에러

TItem 인터페이스가 가지고 있는 속성에 대해서만 인자로 받는다.



profile
다 하자

0개의 댓글