TypeScript - Enum과 제네릭

uk·2023년 5월 24일
1

TypeScript

목록 보기
4/5

Enum(열거형)

enum Animal {
  Cat,
  Dog,
  Rabbit,
}

enum은 상수들의 집합을 정의할 때 사용한다. TypeScript에서는 숫자 열거형과 문자열 열거형을 지원하며 임의의 숫자나 문자열을 할당할 수 있다.

enum을 사용하면 코드의 가독성 향상 시키고 휴먼에러를 방지할 수 있다.


숫자 열거형(Numberic enums)

enum Animal {
  Cat = 1,
  Dog = 5,
  Rabbit = 6
}
  let test1: Animal = Animal.Cat;
  let test2: number = Animal.Rabbit;
  
  console.log(test1);  // 1
  console.log(test2);  // 6

enum은 기본값으로 숫자 열거형을 사용하며 Animal에 정의된 각 멤버들은 순차적으로 0부터 1씩 증가하는 값을 가진다.

직접 number를 지정할 수도 있고 이후의 값은 자동으로 enum에 선언된 순서에 따라 1씩 증가한다.


문자열 열거형(String enums)

enum Animal {
  Cat = 'luna',
  Dog = 'dangdang',
  Rabbit = 'toto'
}

let test: Animal = Animal.Dog;
console.log(test);  // dangdang

Animal에 정의된 멤버들에 문자열 값이 할당되어있다. 문자열 열거형은 숫자 열거형과 거의 비슷하지만 자동으로 증가시키는 기능은 없다.


역 매핑 (Reverse mappings)

enum Test {
  A
}

let a = Test.A;
let nameOfA = Test[a];  // A

숫자 열거형에만 존재하는 특징으로 열거형의 키(key)로 값(value)을 얻을 수 있고 값(value)으로 키(key)를 얻을 수 있다.


enum 사용을 지양해야 하는 이유

  1. 트리쉐이킹(사용하지 않는 코드를 제거) 되지 않는다.

  2. 숫자 열거형은 예기치 못한 오류가 발생할 수 있다.

  3. Enum간 값 비교가 되지않는다.

-> Union Type 사용


제네릭(Generics)

제네릭은 Java, C# 등 정적 언어에서 여러 타입을 받아 재사용 가능한 컴포넌트를 생성할 때 주로 사용한다.

TypeScript에서 함수나 클래스를 정의할 때 매개변수 타입과 리턴 타입을 지정해야한다. 이때 제네릭을 사용하면 타입을 변수화하여 타입을 미리 지정(고정)하지 않고 함수나 클래스를 호출할 때 전달되는 데이터의 타입에 따라 자동으로 타입을 추론한다.

-> 선언 시점이 아닌 호출 시점에 타입이 결정되며 다양한 타입을 받아 재사용된다.


제네릭을 사용하는 이유

function print(text: string): string {
	return text;
}

print('test');  // test
print(1234);  // Argument of type 'number' is not assignable to parameter of type 'string'.

위 예시는 제네릭을 사용하지 않고 고정된 타입 지정하였다. print 함수에 특정 타입을 지정해야 하고 string 타입 외에 다른 타입이 전달되면 컴파일 에러가 발생한다.


함수 중복 선언(함수 오버로딩)

function print(text: string): string 
function print(text: number): number
function print(text: any): any {
  return text
}

print('test');  // test
print(1234);  // 1234

string 외에 다른 타입의 데이터를 받기 위해 함수를 중복으로 선언하여 문제를 해결하였지만 타입이 많아질수록 코드량도 많아지기 때문에 이 방법은 가독성과 유지보수에 좋지 않다.

함수 오버로딩 - 동일한 함수의 매개변수 타입과 리턴 타입을 다르게 하여 다양하게 사용


유니온 타입 사용

function print(text: string | number) {
  return text;
}

print('test');
print(1234);

function add(x: number | string, y: number | string) {
  return x + y  // Operator '+' cannot be applied to types 'string | number' and 'string | number'.
}

유니온 타입을 사용하여 두가지 타입을 받을 수 있게 되었다.

하지만 string, number 타입 둘 다 접근할 수 있기 때문에 타입 추론이 불가능해지고 에러가 발생한다.


any 타입 사용

function print(text: any): any {
	return text;
}

any 타입은 여러가지 타입을 허용하지만 타입 검사를 하지 않기 때문에 반환되는 타입을 추론할 수 없게된다.


제네릭 사용

함수와 제네릭

// 인터페이스를 통해 제네릭 함수 타입 정의
interface Generic {
  <T>(text: T): T;
}

function printText<T>(text: T): T {
	return text;
}

// 화살표 함수로 표기
const printText = <T>(text: T): T => {
  return text;
}

let test: Generic = printText;

test<number>(1234);
test<string[]>(['a', 'b', 'c']);

printText 함수 타입으로 <T>, text 매개변수 타입으로 T, 리턴 타입으로 T 변수(타입)를 추가했다. 제네릭을 통해 타입을 변수화하고 함수 호출 시 전달되는 데이터의 타입에 따라 타입을 결정하는 방식으로 다양한 타입을 안정적으로 재사용할 수 있다.

-> any 타입과 다르게 반환되는 타입을 정확하게 추론할 수 있다.

호출 시 해당 함수 뒤 <> 안에 타입을 명시한다.


interface Generic<T> {
  (text: T): T; 
}

function printText<T>(text: T): T {
	return text;
}

let textNum: Generic<number> = printText;
let textStr: Generic<string> = printText;

textNum(1234);
textStr('test');

변수에 함수를 할당할 때 인터페이스를 통해 제네릭 타입을 결정하는 방식도 가능하다.


let text1 = printText<number>(1234);
let text2 = printText(1234);

두가지 호출 방법 중 text2 방법의 코드가 더 짧고 가독성이 좋기 때문에 많이 사용되나 코드가 복잡해져서 타입 추론이 불가능한 경우 <> 안에 타입을 명시한다.


인터페이스와 제네릭

interface Person<T> {
  name: T;  // 제네릭 타입, name 프로퍼티에 다양한 타입의 데이터가 들어올 경우
  age: number;
  gender: string;
}

let user1: Person<string> = {
  name: 'kim',
  age: 25,
  gender: 'female'
};

let user2: Person<number> = {
  name: 100,
  age: 20,
  gender: 'male'
};
  1. Person 인터페이스에 <T> 변수를 선언하고 제네릭을 사용할 프로퍼티에 제네릭 타입 T를 선언한다.

  2. Person 인터페이스를 타입으로 사용하는 user1, user2 객체의 <> 안에 사용할 타입을 선언한다. -> <>안에 타입을 명시하지 않아도 컴파일러가 알아서 타입을 추론하지만 코드가 복잡하거나 유니온 타입을 사용할 경우 제네릭을 명시하는 것이 좋다.

  3. name 프로퍼티에 선언했던 타입의 값을 넣어주면 인터페이스를 여러개 만들 필요없이 재사용할 수 있게된다.


let user3: Person<{ str: string; num: number; }> = {
  name: { str: 'park', num: 1234 },
  age: 10, 
  gender: 'male'
};

제네릭에 객체 리터럴 형태의 타입도 할당할 수 있다.


타입 별칭과 제네릭

type Person<T> = {
  name: string;
  age: T;
}

let user: Person<number> = {
  name: 'kim',
  age: 30
}

type Test<T> = T[] | T;

let strArr: Test<string> = ['one', 'two', 'three'];
let numARR: Test<number> = [1, 2, 3];
let str: Test<string> = 'test';
let num: Test<number> = 1234;

제네릭 타입 변수

function print<T>(text: T): T {
  console.log(text.length);  // Property 'length' does not exist on type 'T'.
  return text;
}

위 예시는 제네릭 T에 어떤 타입이 들어올지 모르기 때문에(number 타입이 들어온다면 .length를 사용할 수 없다) 컴파일 에러가 발생한다. 이런 경우 제네릭에 타입을 정의해서 사용한다.


// 방법1
function print<T>(text: T[]): T[] {
	console.log(text.length);
	return text;
}

// 방법2
function print<T>(text: Array<T>): Array<T> {
	console.log(text.length);
	return text;
}

배열 타입은 .length를 사용할 수 있다.

print 제네릭 함수는 배열 타입의 제네릭 T[]를 매개변수로 받기 때문에 .length를 사용할 수 있다.


제네릭 제약 조건 (Generic Constraints)

1. extends

type strNum = string | number;

function printStrNum<T extends strNum>(text: T): T {
  return text; 
}

printStrNum('test'));  // test
printStrNum(1234));  // 1234

printStrNum([1, 2, 3]);  // Argument of type 'number[]' is not assignable to parameter of type 'strNum'.
printStrNum(false);  // Argument of type 'boolean' is not assignable to parameter of type 'strNum'.

printStrNum 함수는 제네릭 타입 T를 extends 키워드를 통해 타입 별칭으로 선언한 strNum(유니온 타입)으로 제한한다.


interface Length {
  length: number;
}

function print<T extends Length>(text: T): T {
  console.log(text.length);
  return text;
}

print(1234);  // Argument of type 'number' is not assignable to parameter of type 'Length'.
print(true);  // Argument of type 'boolean' is not assignable to parameter of type 'Length'.

print('test');  // 4
print([1, 2, 3, 4, 5]);  // 5

extends 키워드를 사용하면 제네릭 T를 .length를 사용할 수 있는 타입으로 제한(타입의 종류를 제한)할 수 있다.

제약 조건을 명시하는 인터페이스를 통해 length 프로퍼티를 선언하고 extends 키워드를 사용하여 .length를 사용할 수 있는 타입만 매개변수로 받을 수 있다.

-> 클래스나 인터페이스에서 extends 키워드는 확장을 뜻하지만 제네릭에서 extends 키워드는 제한을 뜻한다.


2. keyof

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

function getProp<T extends keyof Person>(text: T): T {
  return text;
}

getProp('name');
getProp('id');  // Argument of type '"id"' is not assignable to parameter of type 'keyof Person'.

keyof 키워드를 사용해 객체(인터페이스)에 존재하지 않는 프로퍼티에 접근할 수 없도록 제한할 수 있으며 프로퍼티를 잘못 전달하는 실수를 방지한다.


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

// 제네릭은 함수에서 여러개 지정해서 사용할 수 있다.
function getProp<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}
 
let user: Person = { 
  name: 'lee',
  age: 25,
  gender: 'female'
};
 
getProp(user, 'gender');
getProp(user, 'id');  // Argument of type '"id"' is not assignable to parameter of type '"gender" | "name" | "age"'.

getProp 함수의 매개변수인 제네릭 T 타입에는 객체가 들어오는데 keyof 키워드를 통해 객체의 key값(프로퍼티)만 들어올 수 있도록 extends 키워드를 사용해 제네릭 K에 제한을 건다.

-> 제네릭 K는 제네릭 T 타입의 프로퍼티를 반드시 포함해야한다.

0개의 댓글