TIL

2024.03.05 TIL #TypeScript

inz1234 2024. 3. 5. 21:05

enum
- 간단한 상수 값에 적합 (애초에 숫자와 문자열만 취급 가능)
- 열거형 데이터 타입
- 컴파일 시 자동으로 숫자로 매핑되기 때문에 따로 값을 할당할 필요가 X
   (단, 특정 숫자 값으로 매핑되어야 한다면 걔는 직접 할당해주면 됨)


[예시]
enum UserRole {
  ADMIN = "ADMIN",
  EDITOR = "EDITOR",
  USER = "USER",
}

enum UserLevel {
  NOT_OPERATOR, // 0
  OPERATOR // 1
}

function checkPermission(userRole: UserRole, userLevel: UserLevel): void {
  if (userLevel === UserLevel.NOT_OPERATOR) {
    console.log('당신은 일반 사용자 레벨이에요');
  } else {
    console.log('당신은 운영자 레벨이군요');
  } 

  if (userRole === UserRole.ADMIN) {
    console.log("당신은 어드민이군요");
  } else if (userRole === UserRole.EDITOR) {
    console.log("당신은 에디터에요");
  } else {
    console.log("당신은 사용자군요");
  }
}

const userRole: UserRole = UserRole.EDITOR;
const userLevel: UserLevel = UserLevel.NOT_OPERATOR;
checkPermission(userRole, userLevel);


object literal
- 복잡한 구조, 다양한 데이터 타입에 적합

=> 데이터가 숫자나 문자 중 하나이고, 변경될 일이 없을 때 : enum
=> 데이터가 객체를 포함하고 있고, 변경될 일이 많을 때: object

유틸리티
(1) Partial <T>
  - 타입 T(미리 선언해 둔 객체이름) 안의 속성을 선택적으로 만듦
  - 기존 타입의 일부 속성만 제공하는 객체
[예시]
interface Person {
  name: string;
  age: number;
}

const updatePerson = (person: Person, fields: Partial<Person>): Person => { 
   // 인자로 Person 객체를 넣고, fields: Person 객체의 name이나 age 또는 둘 다의 key를 가진 "객체"
  return { ...person, ...fields };
};

const person: Person = { name: "Spartan", age: 30 };
const changedPerson = updatePerson(person, { age: 31 });
- name, age 이외의 {gender: female} 등 없는 key로의 객체는 허용하지 않음 

(2) Required <T>
 - 타입 T의 모든 속성을 필수로 만듦
 - 즉, 모든 Key를 제공하는 객체를 만들어야 함
[예시]
interface Person {
  name: string;
  age: number;
  address?: string; // 속성 명 뒤에 붙는 ?가 뭘까요 : 있어도 되고, 없어도 되는 친구

근데 만약 address를 필수적으로 받아야 한다면?
type RequiredPerson = Required<Person>; // Required를 써서 물음표? 로 되어있는 key가 무조건 있는 객체를 만들겠다.

(3) Readonly <T>
 - 타입 T의 모든 속성을 읽기 전용(read-only)로 만듦
 - 이를 통해 readonly 타입의 속성들로 구성된 객체가 아니어도 완전한 불변 객체로 취급할 수 있음
[예시]
interface DatabaseConfig {
  host: string;
  readonly port: number; // 인터페이스에서도 readonly 타입 사용 가능해요!
}

const mutableConfig: DatabaseConfig = { // mutableConfig: DatabaseConfig 객체를 가지고 만든 객체 
  host: "localhost",
  port: 3306,
};

const immutableConfig: Readonly<DatabaseConfig> = { // mutableConfig: DatabaseConfig 객체로 만들긴 했지만, Readonly로 감싸서 만듦
  host: "localhost",
  port: 3306,
};

mutableConfig.host = "somewhere";
immutableConfig.host = "somewhere"; // 오류! => Readonly로 감쌌기 때문에 원래 값이 "localhost" 였던 immutableConfig.host에 "somewhere"이란 값을 할당할 수 없음
즉, 맘대로 바꿀 수 없다~

(4) Pick <T,K>
 - 타입 T 안의, K 속성들만 선택해서 새로운 "타입"을 만듦
 - 타입의 일부 속성만 포함하는 객체를 쉽게 생성 가능
[예시]
interface Person {
  name: string;
  age: number;
  address: string; // address 필수 key로 지정되어있는데
}

type SubsetPerson = Pick<Person, "name" | "age">; // 나는 SubsetPerson 라는 address key가 없는 객체를 만들고 싶은 거야 

const person: SubsetPerson = { name: "Spartan", age: 30 }; 
// 최종적으로 person이라는 객체를 만들건데, SubsetPerson의 구조처럼 name과 age만 있는 객체를 만들고, 각 값을 할당한 거지

(5) Omit <T, K>
 - Pick <T,K>와 반대로 T라는 객체 안의 K 속성들을 "제외"해서 새로운 "타입" 만들기 
 - 예를 들어서 타입 T에 30개의 key가 있다고 치자. 근데 나는 그 중에 27개만 필요해. 그럼 27개를 pick 하는 게 빠를까 아니면 3개만 제외시키는 게 빠를까. 그럴때 쓰는 거


interface Person {
  name: string;
  age: number;
  address: string;
}

type SubsetPerson = Omit<Person, "address">; // SubsetPerson라는 타입은: Person에서 address를 제외한 타입 객체다.

const person: SubsetPerson = { name: "Alice", age: 30 }; // person이라는 객체는: SubsetPerson라는 타입을 가지고, 각 key에 Alice와 30 할당 

이 외에 다양한 유틸리티 타입 
https://www.typescriptlang.org/ko/docs/handbook/utility-types.html

 

Documentation - Utility Types

Types which are globally included in TypeScript

www.typescriptlang.org

 

>> 클래스
- 객체를 "생성"하고 관리 
- 객체 생성을 위한 설계도(붕어빵 틀) 속성과 메서드를 포함
- 인스턴스화 하여 실제 객체를 생성
  ㄴ 속성: 객체의 성질 
                  - 팥_붕어빵, 슈크림_붕어빵
  ㄴ 메서드 : 객체의 성질을 변화시키거나, 객체에서 제공하는 기능들을 사용하는 창구 
                  - 붕어빵 주인: 팥_붕어빵에서 -> 슈크림_붕어빵으로 전환
                  - 소비자: 팥_붕어빵과 슈크림_붕어빵의 가격을 알 수 있음
- 객체는 클래스를 기반으로 생성되며, 클래스의 인스턴스라고도 함
constructor
  ㄴ 클래스의 인스턴스를 생성하고 속성을 초기화할 때 자동으로 호출되는 특별한 메서드(그 이후에는 안 불림)
  ㄴ 생성자는 클래스 내에 오직 하나만 존재할 수 있음
  ㄴ 속성 초기화 외에도 객체가 생성될 때 디폴트값으로 실행되어야 하는 로직도 넣을 수 있음
      ex) DBConnector라는 클래스가 있다면 이 클래스가 생성될 때, constructor 인자로 host, port 등을 넣어서 DB로 미리 연결

[예시]
class Person {
  name: string;
  age: number;

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

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

const person = new Person('Spartan', 30);
person.sayHello();

>> 접근 제한자
- 속성과 메서드에 접근 제한자를 사용해 접근을 제한
(1) public
   - 클래스 외부에서도 접근 가능
   - 접근 제한자 선언이 안 되어있다면, 자동으로 public(= public은 기본적으로 접근제한자 명시 생략)
(2) private
  - 클래스 내부에서만 접근 가능
  - 보통 클래스의 속성(key)를 private로 설정
  - 혹시나 외부에서 클래스의 속성(key)를 열람 or 편집하고 싶으면 getter/setter를 준비해 두는 것이 관례
(3) portected
  - 클래스 내부 + 클래스를 상속받은 자식 클래스까지도 접근 가능
[예시]
class Person {
  private name: string;
  private age: number;

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

  public sayHello() {
    console.log(`안녕하세요! 제 이름은 ${this.name}이고, 나이는 ${this.age}살입니다.`);
  }
}
// 위의 예시와 다르게 최초에 대입된 this.name 과 this.age로만 sayHello() 함수 호출 가능
// 수정이 필요하다면 코드 맨 밑에 setter를 추가하여 this.name과 this.age를 수정하면 됨

>> 상속
- 객체지향 프로그래밍에서 클래스 간의 관계를 정의하는 개념
- 기존 클래스의 속성과 메서드를 "물려받아" 새로운 클래스 정의하는 것
- 구현 시 "extends" 사용
[예시]
class Animal {
  name: string;

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

  makeSound() {
    console.log('동물 소리~');
  }
}

class Dog extends Animal {  // extends 등장! 상속받겠다! = 나 개인데, 동물을 상속받을거야(즉, name과 makeSound는 이미 물려받음)
  age: number;                    // name과 makeSound 말고도 age는 내가 만들거야

  constructor(name: string) {
    super(name);    // super: 베이스 클래스의 생성자 호출
// 자식 클래스가 부모 클래스의 생성자나 메서드를 그대로 사용하고 싶다면 자식 클래스에선 super 다시 작성하지 않아도 됩니다! → Cat 클래스를 보세요!
    this.age = 5;
  }

  makeSound() {
    console.log('멍멍!'); // 부모의 makeSound 동작과 달라요! makeSound "함수의 동작"을 내가 재정의 ===>  메서드 "오버라이딩"
  }

  eat() {                     // Dog 클래스만의 새로운 함수 정의
    console.log('강아지가 사료를 먹습니다.');
  }
}

class Cat extends Animal { // Animal과 다를게 하나도 없어요! 그냥 베이스 클래스와 똑같이 (이렇게만은 잘 안씀)
}

const dog = new Dog('누렁이');
dog.makeSound(); // 출력: 멍멍!

const cat = new Cat('야옹이');
cat.makeSound(); // 출력: 동물 소리~

>> 서브타입 & 슈퍼타입
- 슈퍼타입: Animal
- 서브타입: Dog, Cat

>> upcasting & downcasting : 슈퍼타입, 서브타입으로 변환할 수 있는 타입변환 기술
(1) upcasting: 서브타입 -> 슈퍼타입 

[예시]
let dog: Dog = new Dog('또순이');
let animal: Animal = dog;            // 슈퍼타입 변수에 서브타입 변수를 할당하면 upcasting 발동!  
animal.eat();                              // 에러. 이제 Dog도 Animal과 똑같이 슈퍼타입(Animal)으로 변환이 되어 Animal이 Dog만의 메서드인 eat을 호출할 수 없어요! 

ㄴ 왜 사용하는 거죠?
  예를 들어 Dog, Cat, Lion 등 다양한 동물이 있다고 치고, 이 동물들을 다 커버할 수 있는 객체를 항상 받고 싶을 때 Animal 부모타입의 객체로 선언만 해주면 모두 받을 수 있죠

(2) downcasting : 슈퍼타입 -> 서브타입
[예시]
let animal: Animal;   
animal = new Dog('또순이');

let realDog: Dog = animal as Dog;  // 난 원래 Animal 타입(슈퍼)이지만 Dog(서브타입)로 변신할거야! 한다면 => as 키워드로 명시적으로 타입 변환해줘야 함!
realDog.eat(); // 서브타입(Dog)로 변환이 되었기 때문에 eat 메서드를 호출할 수 있죠!

ㄴ 왜 사용하는 거죠?
    서브타입만의 메서드를 사용하고 싶을 때!

>> 추상클래스(강하게 키운다)
- 클래스와는 다르게 인스턴스화 할 수 "없"음 ( = new ~~ - 이렇게 인스턴스를 생성할 수 없음)
- 그럼 왜 써요? 상속을 통해서 자식 클래스에게 메서드를 제각각 구현하도록 강제하기 = 기본 메서드만 엄마가 해주고 나머지 핵심 메서드는 자식이 구현하도록 위임

- abstract 키워드 사용
- 1개 이상의 "추상함수"가 있는 것이 일반적
[예시]
abstract class Shape {              //  abstract => 추상 클래스와
  abstract getArea(): number;     // abstract => 추상 함수 정의!!!

  printArea() {                      // 그냥 함수
    console.log(`도형 넓이: ${this.getArea()}`);
  }
}

class Circle extends Shape {  // 엄마한테 상속받아서 자식1 뿅
  radius: number;

  constructor(radius: number) {
    super();
    this.radius = radius;
  }

  getArea(): number { // 원의 넓이를 구하는 공식은 파이 X 반지름 X 반지름
                            => *** getArea 애를 반드시 구현해줘야 함!! *** 추상클래스를 상속받았기 때문에 추상 함수도 반드시 자식 안에서 구현해줘야 함
    return Math.PI * this.radius * this.radius;
  }
}

class Rectangle extends Shape {  // 엄마한테 상속받아서 자식2 뿅
  width: number;
  height: number;

  constructor(width: number, height: number) {
    super();
    this.width = width;
    this.height = height;
  }

  getArea(): number { // 사각형의 넓이를 구하는 공식은 가로 X 세로
    return this.width * this.height;
  }
}

const circle = new Circle(5);
circle.printArea();

const rectangle = new Rectangle(4, 6);
rectangle.printArea(); 

>> 인터페이스
- 객체의 "타입"을 정의하는 데에 사용됨 (걔의 타입이 변수인지, 함수인지, 객체인지 알려주는)
- 어떤 속성과 어떤 메서드를 가져야하는지, 요구사항 명시
- 인터페이스로 구현한 객체는 반드시 인터페이스를 "규약처럼" 준수해야 함!
- 실제로 인터페이스는 코드를 컴파일할 때 제거됨, JS로 변환되면 사라짐

[예시]
interface Car {
  brand: string;
  model: string;
  start(): void;
  stop(): void;
}

const myCar: Car = {
  brand: "Toyota",
  model: "Corolla",
  start() {
    console.log("Engine started");
  },
  stop() {
    console.log("Engine stopped");
  },
};


- 인터페이스 vs 추상클래스
  ㄴ 기본구현 제공여부
      추상클래스 : 클래스의 기본구현 제공
      인터페이스 : 객체의 구조만 정의, 기본구현 제공 X
  ㄴ 상속
      추상클래스 : 단일 상속만 지원 (차는 "탈 것" 만 상속받을 수 있음. "날 것"은 상속받을 수 없음)
      인터페이스 : 다중 상속 = 하나의 클래스는 여러 인터페이스를 구현할 수 있음
  ㄴ 구현
      추상클래스 : 추상클래스(엄마)로부터 상속받은 자식클래스는 반드시 엄마한테 있는 추상함수를 구현해야 함
      인터페이스 : 인터페이스(엄마)를 구현하는 모든 클래스들은 인터페이스에 정의된 모든 메서드를 직접 구현해야 함(개발자가 해야할 일이 좀 더 많음) 

=> 기본구현을 제공하고 상속을 통해 확장하고 싶다면? 추상클래스
=> 객체가 완벽하게 특정 구조를 준수하도록 강제하고 싶다면? 인터페이스