object literal ( 객체 리터럴 ) : const , let 사용 가능 -> 키+값 페어로 구성된 객체 정의 방식
예시 코드 -> 어떤 타입의 값도 대입 가능 -> 코드 사용 전 값이 할당 되어야하므로 런타임 에러 방지 가능
const obj = {
a: [1, 2, 3],
b: "b",
c: 4,
};
enum과 사용 구별
enum은 간단한 상수 값 그룹 관리 시 적합 / 상수이므로 각 멤버의 값이 변하면 안됨
객체 리터럴은 멤버의 값이나 데이터 타입 변경 가능 / 복잡한 구조와 다양한 데이터 타입 사용 시 사용
유틸리티 타입
Partial<T> : 타입 T(제네릭)의 모든 속성을 선택적으로 만듦 -> 기존 타입의 일부 속성만 제공하는 객체 생성
예시 코드
// interface는 TS에서 객체의 구조 정의 시 사용 -> 객체가 어떤 속성과 타입을 가져야하는지 명확하게 규정 가능
interface Person {
name: string;
age: number;
}
const updatePerson = (person: Person, fields: Partial<Person>): Person => {
return { ...person, ...fields };
};
const person: Person = { name: "Spartan", age: 30 };
// name과 age 속성 중 택1 혹은 둘 다 인자로 구성 가능
const changedPerson = updatePerson(person, { age: 31 });
Required<T> : Partial<T> 타입과 반대로 타입T의 모든 속성을 필수적으로 만듦
-> T타입 객체에 정의된 모든 속성이 반드시 전부 제공 되는 객체를 생성해야함
예시 코드
interface Person {
name: string;
age: number;
address?: string; // 속성 명 뒤에 붙는 ?는 선택적 속성 -> 있어도 되고 없어도 됨
}
// Required 사용 시 address도 필수로 입력 해야함
type RequiredPerson = Required<Person>;
Readonly<T> : 모든 속성을 읽기 전용(객체의 상수(const)화)으로 만듦
-> readonly 타입의 속성들로 구성된 객체가 아니어도 완전한 불변 객체로 취급 가능
예시 코드 -> 초기 Data... 객체는 불변X (host가 readonly가 아님) -> Readonly타입으로 불변 객체가 됨
interface DatabaseConfig {
host: string;
readonly port: number; // 인터페이스에서도 readonly 타입 사용 가능
}
const mutableConfig: DatabaseConfig = {
host: "localhost",
port: 3306,
};
const immutableConfig: Readonly<DatabaseConfig> = {
host: "localhost",
port: 3306,
};
mutableConfig.host = "somewhere";
immutableConfig.host = "somewhere"; // 오류!
Pick<T,K> : 타입 T에서 K(일부)속성들만 선택하여 새로운 타입을 만듦
-> 타입의 일부 속성만을 포함하는 새로운 타입을 생성
예시 코드 -> Sub...타입은 Person 인터페이스에서 name,age 속성만 선택하여 구성한 새로운 타입임
interface Person {
name: string;
age: number;
address: string;
}
type SubsetPerson = Pick<Person, "name" | "age">;
const person: SubsetPerson = { name: "Spartan", age: 30 };
Omit<T,K> : 타입 T에서 K 속성들만 제외하여 새로운 타입을 만듦
-> Pick과 반대 -> 특정 타입을 제거한 새로운 타입을 생성
예시 코드 -> Sub...타입은 Person 타입( interface라 부르는 것이 적절하지만 interface가 타입의 일부이므로 타입이라고 부르는 것도 맞긴 함 ) 에서 address 속성만 제외한 새로운 타입임
interface Person {
name: string;
age: number;
address: string;
}
type SubsetPerson = Omit<Person, "address">;
const person: SubsetPerson = { name: "Alice", age: 30 };
간단한 카페 코드
interface User {
id: number;
name: string;
role: "admin" | "customer";
}
interface Drink {
name: string;
price: number;
}
interface Order {
orderId: number;
customerId: number;
customerName: string;
drinkName: string;
// union 타입 -> 세가지 string 중 하나를 가질 수 있다는 뜻
// string 으로 쓰지 않는건 다른 문자열을 막기 위해서임
status: "placed" | "completed" | "picked-up";
}
// 관리 할 데이터 선언
let drinks: Drink[] = [];
let orders: Order[] = [];
const isAdmin = (user: User): boolean => {
return user.role === "admin";
};
const isCustomer = (user: User): boolean => {
return user.role === "customer";
};
const addDrink = (user: User, name: string, price: number): void => {
if (!isAdmin(user)) {
console.log("권한이 없습니다.");
return;
}
const newDrink: Drink = { name, price };
drinks.push(newDrink);
};
const removeDrink = (user: User, drinkName: string): void => {
if (!isAdmin(user)) {
console.log("권한이 없습니다.");
return;
}
drinks = drinks.filter((drink) => drink.name !== drinkName);
};
const getDrinks = (user: User): Drink[] => {
if (!user) return [];
return drinks;
};
const findDrink = (drinkName: string): Drink | undefined => {
return drinks.find((drink) => drink.name === drinkName);
};
// 주문Id를 반환
const placeOrder = (user: User, drinkName: string): number => {
if (!isCustomer(user)) {
console.log("권한이 없습니다.");
return -1;
}
const drink = findDrink(drinkName);
if (!drink) {
console.log("해당 음료가 없습니다.");
return -1;
}
const newOrder: Order = {
orderId: orders.length + 1,
customerId: user.id,
customerName: user.name,
drinkName,
status: "placed",
};
orders.push(newOrder);
return newOrder.orderId;
};
const completeOrder = (user: User, orderId: number): void => {
if (!isAdmin(user)) {
console.log("권한이 없습니다.");
return;
}
const order = orders.find((order) => order.orderId === orderId);
if (order) {
order.status = "completed";
console.log(
`[고객 메시지] ${order.customerName}님 주문하신 ${order.drinkName}나왔습니다`
);
}
};
const pickUpOrder = (user: User, orderId: number): void => {
if (!isCustomer(user)) {
console.log("권한이 없습니다.");
return;
}
const order = orders.find(
(order) => order.orderId === orderId && order.customerId === user.id
);
if (order && order.status === "completed") {
order.status = "picked-up";
console.log(
`[어드민 메시지] 고객 ID[${order.customerId}]님이 주문 ID[${orderId}]을 수령했습니다.`
);
}
};
function main() {
const admin: User = {
id: 1,
name: "바리스타",
role: "admin",
};
// 유저 생성
const member1: User = {
id: 2,
name: "르탄이",
role: "customer",
};
const member2: User = {
id: 3,
name: "꿈꾸는개발자",
role: "customer",
};
// 음료 등록
addDrink(admin, "아메리카노", 4000);
addDrink(admin, "카페라떼", 4500);
addDrink(admin, "에스프레소", 3000);
// 음료 삭제
removeDrink(admin, "에스프레소");
console.log(
`안녕하세요~ ${
member1.name
} 고객님! 별다방에 오신 것을 환영합니다. 저희는 ${JSON.stringify(
getDrinks(member1)
)}를 판매하고 있습니다.`
);
// 음료 주문
const orderId1 = placeOrder(member1, "아메리카노");
if (orderId1 > 0) {
setTimeout(() => {
// 음료 제작 완료
completeOrder(admin, orderId1);
// 음료 수령
pickUpOrder(member1, orderId1);
}, 1000);
}
console.log(
`안녕하세요~ ${
member2.name
} 고객님! 별다방에 오신 것을 환영합니다. 저희는 ${JSON.stringify(
getDrinks(member2)
)}를 판매하고 있습니다.`
);
// 음료 주문
const orderId2 = placeOrder(member2, "카페라떼");
if (orderId2 > 0) {
setTimeout(() => {
// 음료 제작 완료
completeOrder(admin, orderId2);
// 음료 수령
pickUpOrder(member2, orderId2);
}, 3000);
}
}
main();
클래스
클래스는 객체 지향 프로그래밍 핵심 요소 중 하나이며 클래스는 객체를 만들기 위한 틀
☑️ 클래스의 구성 요소
- 클래스에서는 같은 종류의 객체들이 공통으로 가지는 속성(attribute)과 메서드(method)를 정의
- 속성은 객체의 성질을 결정하는 것
- 예를 들어, 붕어빵은 팥이란 속성이 있는 팥 붕어빵 과 슈크림 이란 속성이 있는 슈크림 붕어빵이 있음
- 메서드는 객체의 성질을 변화시키거나 객체에서 제공하는 기능들을 사용하는 창구.
- 붕어빵 주인은 붕어빵을 팥 붕어빵에서 슈크림 붕어빵으로 전환 가능.
- 붕어빵을 사는 고객들은 팥 붕어빵, 슈크림 붕어빵의 가격을 알 수 있음.
☑️ 객체란?
객체는 클래스를 기반으로 생성되며 클래스의 인스턴스(instance)라고도 함
- 클래스 정의하기
- ☑️ 클래스 및 객체 정의 방법
- TypeScript에서 클래스를 정의하려면 class 키워드를 사용하면 됨
- 클래스의 속성과 메서드를 정의하고, new 키워드를 사용하여 객체를 생성 가능.
- 예시 코드
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()
- 생성자(constructor)
- 생성자는 클래스의 인스턴스를 생성하고 초기화하는데 사용되는 특별한 메서드
- 생성자는 클래스 내에서 constructor라는 이름으로 정의됨
- 생성자는 인스턴스를 생성할 때 자동으로 호출됨.
- 생성자는 클래스 내에 오직 하나만 존재 가능.
- 보통, 생성자로 객체 속성을 초기화 하는것 뿐 아니라 객체가 생성이 될 떄 꼭 되어야 하는 초기화 로직을 집어넣기도 함
- 예를 들어, DBConnector라는 클래스가 있다면 이 클래스 타입의 객체가 생성이 될 때 생성자에서 DB 연결을 미리 해주면 편할 것임
☑️ 접근 제한자
- 클래스에서는 속성과 메서드에 접근 제한자를 사용해 접근을 제한 가능.
- TypeScript에서는 다음의 접근 제한자들을 제공
- public
- 클래스 외부에서도 접근이 가능한 접근 제한자
- 접근 제한자가 선언이 안되어있다면 기본적으로 접근 제한자는 public
- 보통은 클래스의 함수 중 민감하지 않은 객체 정보를 열람할 때나 누구나 해당 클래스의 특정 기능을 사용해야 할 때 많이 쓰임
- private
- 클래스 내부에서만 접근이 가능한 접근 제한자.
- 보통은 클래스의 속성은 대부분 private으로 접근 제한자를 설정
- 즉, 외부에서 직접적으로 객체의 속성을 변경할 수 없게 제한하는 것
- 클래스의 속성을 보거나 편집하고 싶다면 별도의 getter/setter 메서드를 준비해놓는 것이 관례
- protected
- 클래스 내부와 해당 클래스를 상속받은 자식 클래스에서만 접근이 가능한 접근 제한자.
- 사용 사례
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}살입니다.`
);
}
}
상속 ( inheritance )
- 상속은 객체 지향 프로그래밍에서 클래스 간의 관계를 정의하는 중요한 개념
- 상속을 통해 기존 클래스의 속성과 메서드를 물려받아 새로운 클래스를 정의 가능
- 상속이 있어서 똑같은 코드를 계속 반복적으로 작성할 필요 없음
- 상속을 구현하려면 extends 키워드를 사용하면 됨
- 예시 코드 -> Animal이 부모 클래스 , Dog가 자식 클래스 / super 키워드는 자식 클래스가 부모 클래스를 참조하는데 사용 -> 자식 클래스에서 생성자를 정의 할 때 부모 클래스의 생성자를 호출해야 하는데 이 때 사용
-
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
makeSound() {
console.log('동물 소리~');
}
}
class Dog extends Animal {
age: number;
constructor(name: string) {
super(name);
this.age = 5;
}
makeSound() {
console.log('멍멍!'); // 부모의 makeSound 동작과 다름
}
eat() { // Dog 클래스만의 새로운 함수 정의
console.log('강아지가 사료를 먹습니다.');
}
}
class Cat extends Animal { // Animal과 다를게 하나도 없음
}
const dog = new Dog('누렁이');
dog.makeSound(); // 출력: 멍멍!
const cat = new Cat('야옹이');
cat.makeSound(); // 출력: 동물 소리~
- Dog 클래스는 부모의 makeSound 함수의 동작을 새롭게 정의 -> 오버라이딩 이라 부름
서브타입 , 슈퍼타입
upcasting과 downcasting은 슈퍼타입, 서브타입으로 변환할 수 있는 타입 변환 기술
☑️ upcasting 예시 코드 : 서브타입 -> 슈퍼타입 변환이 업캐스팅 / 타입 변환은 암시적으로 이루어져 별도 타입 변환 구문 필요X -> TS가 자동으로 변환해주므로 대입만 해도 됨
let dog: Dog = new Dog('또순이');
let animal: Animal = dog; // upcasting 발동!
animal.eat(); // 에러. 슈퍼타입(Animal)으로 변환이 되어 eat 메서드를 호출할 수 없어요!
- upcasting이 필요한 이유는 서브타입 객체를 슈퍼타입 객체로 다루면 유연하게 활용할 수 있기 때문
- 예를 들어, Dog, Cat, Lion 그리고 기타 등등 다양한 동물을 인자로 받을 수 있는 함수를 만들고 싶다면?
- 올바른 선택: 아! Animal 타입의 객체를 받으면 모두 다 받을 수 있음!
- 잘못된 선택: 아! union으로 새로운 타입을 만들어서 해당 타입의 객체를 받게해야하나?
☑️ downcasting 예시 코드 : 슈퍼타입 -> 서브타입 변환이 다운캐스팅 / as 키워드로 명시적으로 타입 변환 해야 함 / 생각보다 잘 안 쓰임
let animal: Animal;
animal = new Dog('또순이');
let realDog: Dog = animal as Dog;
realDog.eat(); // 서브타입(Dog)로 변환이 되었기 때문에 eat 메서드를 호출할 수 있죠!
추상 클래스
☑️ 추상 클래스의 정의
- 추상 클래스는 클래스와는 다르게 인스턴스화를 할 수 없는 클래스
☑️ 추상 클래스가 있는 이유
- 추상 클래스의 목적은 상속을 통해 자식 클래스에서 메서드를 제각각 구현하도록 강제를 하는 용도
- 물론, 추상 클래스도 최소한의 기본 메서드는 정의를 할 수 있음
- 하지만, 골자는 핵심 기능의 구현은 전부 자식 클래스에게 위임을 하는 것
☑️ 추상 클래스 사용 방법
- 추상 클래스 및 추상 함수는 abstract 키워드를 사용하여 정의
- 추상 클래스는 1개 이상의 추상 함수가 있는 것이 일반적.
☑️ 사용 예시
abstract class Shape {
abstract getArea(): number; // 추상 함수 정의!!!
printArea() {
console.log(`도형 넓이: ${this.getArea()}`);
}
}
class Circle extends Shape {
radius: number;
constructor(radius: number) {
super();
this.radius = radius;
}
getArea(): number {
// 원의 넓이를 구하는 공식은 파이 X 반지름 X 반지름
return Math.PI * this.radius * this.radius;
}
}
class Rectangle extends Shape {
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();