Section4 / Unit5 : TypeScript(열거형, 인터페이스, 클래스, 제네릭)
열거형(Enum)
특정 값의 집합을 정의할 때 사용
JavaScript에서는 열거형 지원 X
TypeScript에서는 문자형 열거형과 숫자형 열거형을 지원 O → 디폴트 값으로 숫자형을 사용
숫자형 열거형(Enum)
각 값은 자동으로 0부터 시작하여 1씩 증가한다.
수동으로 값을 지정할 수도 있음
//수동으로 값 지정 가능
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)
숫자형 열거형 개념과 거의 비슷하지만,
문자형 열거형은 열거형의 값을 전부 다 특정 문자 또는 다른 열거형 값으로 초기화해야 한다.
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
let myDirection: Direction = Direction.Up;
console.log(myDirection); // 출력: "UP"
문자형 열거형에는 숫자형 열거형과는 다르게 auto-increasing이 없다.
대신 디버깅을 할 때 숫자형 열거형의 값은 가끔 불명확하게 나올 때가 있지만
문자형 열거형은 항상 명확한 값이 나와 읽기 편합니다.
문자열 기반의 열거형은 주로 외부에서 가져온 값을 TypeScript에서 다루기 위해 사용된다.
ex) HTTP 요청 방식을 나타내는 열거형 정의 가능
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)
타입 체크를 위해 사용
변수, 함수, 클래스에 사용할 수 있으며,
인터페이스에 선언된 프로퍼티 또는 메서드의 구현을 강제하여 일관성을 유지
변수와 인터페이스
TypeScript에서 인터페이스는 객체(Object)의 구조를 정의하기 위해 주로 사용되는 예약어
//인터페이스의 이름을 대문자로 작성할 것!
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"
}
함수와 인터페이스
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);
Greeting 인터페이스에서 이미 greet의 매개 변수인 user와 greeting의 타입과 반환 타입이 작성되어 있기 때문에,
greet 함수는 string 타입을 반환한다고 명시하지 않아도 되며, 매개 변수의 타입 또한 작성하지 않아도 됩니다.
클래스와 인터페이스
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();
console.log(caculator.add(4, 9)); //13
console.log(caculator.substract(10, 5)); //5
SimpleCalculator 클래스 내에는 Calculator 인터페이스를 사용하고 있기 때문에
Calculator 인터페이스 내에 정의된 두 메서드를 반드시 작성해야 한다.
인터페이스와 상속
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}.`);
}
}
이같이 인터페이스도 extends라는 키워드를 사용하여 기존에 존재하던 인터페이스를 상속해 확장이 가능!
장점
✔️ 기존에 존재하던 인터페이스의 프로퍼티를 다른 인터페이스에 복사하는 것을 가능하게 함
✔️ 인터페이스의 재사용성을 높여줌
interface Person {
name: string;
age: number;
}
interface Developer extends Person {
language: string;
}
const person: Developer = {
language: "TypeScript",
age: 20,
name: "Anna",
}
타입 단언(type assertion)
컴파일러에게 특정 타입 정보의 사용을 강제하게 함으로써,
컴파일러가 가진 정보를 무시하고 프로그래머가 원하는 임의의 타입을 값에 할당할 수 있다.
주의!
✔️ 타입 에러만 없애줄 뿐, 런타임 에러까지는 막아주지 않는다.
✔️ 타입 단언이 꼭 필요한 경우가 아니라면 안전성 체크도 되는 타입 선언을 사용하는 것이 좋다.
interface FoodStuff {
name: string;
}
interface FoodAmount {
amount: number;
}
interface FoodFreshness extends FoodStuff, FoodAmount {
isFreshed: boolean;
}
const food = {} as FoodFreshness;
food.name = "egg";
food.amount = 2;
food.isFreshed = true;
타입 별칭(Type Aliases)
타입의 새로운 이름을 만드는 것
새로운 이름으로 기존의 타입을 참조하는 것을 의미한다. → type 키워드를 사용
type MyString = string;
let str1: string = 'hello!';
// string 타입처럼 사용할 수 있습니다.
let str2: MyString = 'hello world!';
타입을 정의할 수 있는 모든 곳에서 타입 별칭을 쓸 수 있다.
장점!
✔️ 코드를 더 간결하고 가독성 좋게 만들 수 있다.
✔️ 복잡한 타입을 간력하게 표현하고,
✔️ 타입 정의를 재사용하는 등 가독성을 높일 수 있다.
type Person = {
id: number;
name: string;
email: string;
}
//Commentary 인터페이스에서 Person 타입을 참조하고 있습니다.
interface Commentary {
id: number;
content: string;
user: Person;
}
//객체에서 Commentary 인터페이스를 참조하고 있습니다.
let comment1: Commentary = {
id: 1,
content: "뭐예요?",
user: {
id: 1,
name: "김코딩",
email: "kimcoding@codestates.com",
},
}
//Commentary 인터페이스 내부에 content 프로퍼티가 존재하기 때문에
//content 프로퍼티를 작성하지 않으면 컴파일 에러가 납니다.
let kimcoding: Commentary = {
id: 1,
user: {
id: 1,
name: "김코딩",
email: "kimcoding@codestates.com",
},
};
//Person 타입 내부에 isDeveloper 프로퍼티가 존재하지 않기 때문에
//isDeveloper 프로퍼티를 작성할 시 컴파일 에러가 납니다.
let kimcoding: Commentary = {
id: 1,
content: "뭐예요?",
user: {
id: 1,
name: "김코딩",
email: "kimcoding@codestates.com",
isDeveloper: true,
},
};
타입 별칭으로 만들어진 타입을 참조할 시에는 인터페이스와 마찬가지로 내부에 정의된 프로퍼티를 전부 참조해야만 합니다.
인터페이스 vs 타입 별칭
타입 별칭 또한 인터페이스와 같은 특징이 있기 때문에, 인터페이스의 역할을 타입 별칭이 수행할 수도 있습니다. 그러나 인터페이스와 타입 별칭에는 미묘한 차이점이 있습니다.
type Person = {
name: string;
age: number;
}
interface User {
name: string;
age: number;
}
let kimcoding: Person = {
name: '김코딩',
age: 30,
}
let coding: User = {
name: '김코딩',
age: 30,
}
각각의 타입과 인터페이스에 마우스를 올려보면
VSCode로 작성 시, type은 내부에 어떤 프로퍼티들이 정의되어 있는지 볼 수 있다.
interface는 안 보임
type Person = {
name: string;
age: number;
}
interface User {
name: string;
age: number;
}
//에러가 발생합니다.
type Students extends Person {
className: string;
}
//정상적으로 동작합니다.
interface Students extends User {
className: string;
}
//정상적으로 동작합니다.
interface Students extends Person {
className: string;
}
타입 별칭은 말 그대로 타입에 새로운 이름을 부여하는 것에 그치기 때문에 확장이 안 된다.
유연한 코드 작성을 위해서는 인터페이스로 만들어서 필요할 때마다 확장할 수 있다.
타입 추론(Type Inference)
변수나 함수의 타입을 선언하지 않아도 TypeScript가 자동으로 유추하는 기능
TypeScript는 정적타입을 지원하는 프로그래밍 언어입니다.
정적타입 시스템을 사용하면 코드의 안정성을 높이고 디버깅을 용이하게 할 수 있다.
TypeScript는 타입 추론(Type Inference)이라는 기능을 통해 코드 작성을 도와줍니다.
타입 추론의 기본
let isNumber = 123;
위의 경우 타입스크립트는 isNumber의 타입을 자동으로 숫자(Number)로 추론한다.
최적 공통 타입(Best common type)
TypeScript는 여러 표현식에서 타입 추론이 발생할 때, 해당 표현식의 타입을 사용하여 최적 공통 타입을 계산한다.
let x = [0, 1, null];
위 예시에서 x 타입을 추론하려면 각 배열 요소의 타입을 고려해야 한다. (number, null)
최적 공통 타입 알고리즘은 각 후보의 타입을 고려하여, 모든 후보의 타입을 포함할 수 있는 타입을 선택한다.
문맥상의 타이핑(Contextual Typing)
타입스크립트에서 타입을 추론하는 또 하나의 방식은 바로 문맥상으로 타입을 결정하는 것다.
이 문맥상의 타이핑(타입 결정)은 코드의 위치(문맥)를 기준으로 일어납니다.
function add(a, b) {
return a + b;
}
add 함수는 두 개의 매개변수를 받아 더한 값을 반환한다.
하지만 매개변수의 타입이 명시되어 있지 않은데
이 경우, 타입스크립트는 매개변수 a와 b의 타입을 자동으로 추론한다.
만약 매개변수 a와 b가 모두 숫자(Number) 타입이라면, add 함수의 반환 값도 숫자(Number) 타입으로 추론됩니다.
타입 추론의 장점
- 코드의 가독성 향상
타입 추론을 사용하면 코드의 가독성이 향상됩니다.
명시적으로 타입을 지정하지 않아도 코드에서 변수의 타입을 알 수 있기 때문입니다. - 개발 생산성 향상
타입 추론을 사용하면 코드 작성 시간을 단축할 수 있습니다.
명시적으로 타입을 지정하지 않아도 TypeScript가 자동으로 타입을 추론하기 때문입니다. - 오류 발견 용이성
타입 추론을 사용하면 코드의 오류를 발견하는 것이 쉬워집니다.
TypeScript는 변수나 함수의 타입을 추론하여 타입 검사를 수행하기 때문입니다.
타입 추론의 단점
- 타입 추론이 잘못될 경우 코드 오류 발생
타입 추론은 TypeScript가 자동으로 수행하는 것이기 때문에,
추론이 잘못될 경우 코드 오류가 발생할 수 있습니다. - 명시적인 타입 지정이 필요한 경우가 있음
타입 추론만으로는 부족한 경우가 있습니다.
특히, 복잡한 함수나 객체의 경우에는 명시적인 타입 지정이 필요할 수 있습니다.
TypeScript의 클래스(Class)
JavaScript와 TypeScript 모두 객체 지향 프로그래밍을 지원하며 클래스(class)를 사용하여 객체를 생성할 수 있습니다.
JavaScript에서의 클래스(Class)
객체를 생성하고 객체의 속성과 메서드를 정의할 수 있습니다.
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)
JavaScript의 클래스와 비슷하지만 몇 가지 추가된 기능이 있다.
✔️ 클래스의 속성과 메서드에 대한 타입을 명시할 수 있다.
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}살 입니다.`);
}
}
// 객체 생성방법은 JavaScript와 비슷
const person = new Person('Alice', 30);
person.greet(); // "안녕하세요, 제 이름은 Alice이고, 30살 입니다."
!!!차이점!!!
✔️ constructor를 이용하여 초기화하는 멤버들은 전부 상단에서 정의를 해줘야 한다.
✔️ constructor 내 인자로 받을 때도 정확히 타입을 명시해줘야 한다.
클래스와 상속(Ingeritance)
extends 키워드를 사용하여 상속 가능
//Animal 클래스 = 기초 클래스, 상위클래스(superclasses)
class Animal {
move(distanceInMeters: number): void {
console.log(`${distanceInMeters}m 이동했습니다.`);
}
}
//Dog 클래스는 = 파생 클래스, 하위클래스(subclasses)
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 키워드를 사용하여 프로퍼티를 읽기 전용으로 만들 수 있다.
읽기 전용 프로퍼티들은 선언 또는 생성자에서 초기화해야 한다. (약간 const 개념?)
class Mydog {
readonly name: string;
constructor(theName: string) {
this.name = theName;
}
}
let spooky = new Mydog("스푸키");
spooky.name = "멋진 스푸키"; // 에러
이런 식으로 변경되면 안 될 값을 readonly로 명시하여 보호할 수 있다.
제네릭(Generic)
타입스크립트의 제네릭(Generic)은 코드 재사용성을 높이고 타입 안정성을 보장하는 기능
제네릭을 사용하면 함수나 클래스를 작성할 때, 사용될 데이터의 타입을 미리 지정하지 않고,
이후에 함수나 클래스를 호출할 때 인자로 전달된 데이터의 타입에 따라 자동으로 타입을 추론하게 됩니다.
제네릭의 필요성
function printLog(text) {
return text;
}
이를 제네릭 없이 구현한다면?
function printLog(text: string): string {
return text;
}
printLog('hello'); // 정상
printLog(123); //에러
//컴파일 에러를 막기 위해 중복으로 함수를 선언하는 방법도 있을 것이다.
function printLog(text: string): string {
return text;
}
function printLogNumber(text: number): number {
return text;
}
printLog('hello'); // 정상
printLogNumber(123); //정상
//타입의 가독성 및 유지보수성이 나쁘다.
//혹은 | 연산자를 이용해 유니온 타입으로 선언하는 방법!
function printLog(text: string | number) {
return text;
}
printLog('hello'); // 정상
printLogNumber(123); //정상
//인수는 해결되지만, 함수 내에서 결국 string과 number가 둘 다 접근할 수 있는 API만 제공
//이 외에는 타입이 정확히 추론되지 않기 때문에 사용 불가능
//any타입
function printLog(text:any): any {
return text;
}
//어떤 타입이든 받을 수 있지만 실제로 함수가 반환할 때 어떤 타입인지 추론 불가능
중복으로 사용하든, 유니온 타입, any 타입을 사용하든 단점이 발생하게 되는데
이 때 제네릭을 사용한다면?
function printLog<T>(text: T): T {
return text;
}
T는 유저가 준 파라미터의 타입을 캡처하고, 이 정보를 나중에 사용할 수 있게 한다.
여기에서는 T를 반환 타입으로 다시 사용.
→ 따라서 파라미터와 반환 타입이 같은 타입을 사용하고 있는 것을 확인할 수 있다.
printLog 함수는 타입을 불문하고 동작하므로 제네릭이라 할 수 있다.
any를 쓰는 것과는 다르게 인수와 반환 타입에 string을 사용한 첫 번째 printLog 함수만큼 정확합니다.
→ 즉, 타입을 추론할 수 있게 된다.
//사용 예시
const str = printLog<string>('hello');
//타입 추론 기능을 활용해 작성한 코드
const str = printLog('hello');
//전달하는 인수에 따라 컴파일러가 자동으로 T의 값을 정하는 방법
//이는 타입이 복잡해져 컴파일러가 타입을 유추할 수 없게 되는 경우에는 사용할 수 없음
인터페이스와 제네릭
interface Item<T> {
name: T;
stock: number;
selected: boolean;
}
//이처럼 작성하면 Item 인터페이스는 여러 개 만들지 않고도 재사용 가능!
const obj: Item<string> = {
name: "T-shirts",
stock: 2,
selected: false
};
const obj: Item<number> = {
name: 2044512,
stock: 2,
selected: false
};
클래스와 제네릭
제네릭을 사용하는 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; };
이것은 GenericNumber 클래스의 문자 그대로 사용하지만 number 타입만 쓰도록 제한하는 것은 없다.
대신 string이나 훨씬 복잡한 객체를 사용할 수 있다.
제네릭 타입 변수
제네릭을 사용하기 시작하면, printLog와 같은 제네릭 함수를 만들 때,
컴파일러가 함수 본문에 제네릭 타입화된 매개변수를 쓰도록 강요합니다.
function printLog<T>(text: T): T {
console.log(text.length);
return text;
}
//console.log(text.length);를 작성하게 되면 컴파일 에러가 납니다.
//왜냐하면 개발자가 string 타입이 아닌 number 타입을 보낼 수도 있기 때문에,
//T에는 .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;
}
제네릭 제약 조건
앞서 제네릭 타입 변수 외에도 제네릭 함수에 어느 정도 어떤 타입이 들어올 것인지 힌트를 줄 수 있습니다.
function printLog<T>(text: T): T {
console.log(text.length);
return text;
}
인자의 타입에 선언한 T는 아직 어떤 타입인지 구체적으로 정의하지 않았기 때문에 length 코드에서 오류가 난다.
이럴 때 만약 해당 타입을 정의하지 않고도 length 속성 정도는 허용하려면 아래와 같이 작성!
interface TextLength {
length: number;
}
function printLog<T extends TextLength>(text: T): T {
console.log(text.length);
return text;
}
이와 같이 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 extends keyof Item> 부분에서 첫 번째 인자로 받는 객체에 없는 속성들은 접근할 수 없게끔 제한할 수 있다.