1. 왜 제네릭이 필요한가?
// 제네릭
function func0(value: any){
return value;
}
let num0 = func(10);
let bool0 = func(true);
let str0 = func("string");
// 이렇게 매개변수로 받은 타입의 변수를 리턴하는 함수를 만들고 싶을 때, any를 사용해서 할 순 있지만, 이는 위험함!!
// -> 이럴 때 사용하는 것이 제네릭 함수임!!
- any는 유연하지만 타입 안정성을 잃음
- unknown은 안전하지만 사용 전에 타입 좁히기가 필요!!
function func(value: unknown) {
return value;
}
let num = func(10);
if (typeof num === "number") {
num.toFixed(); // 안전하지만 번거로움
}
2. 제네릭 함수
// 제네릭 함수 -> 포괄적, 일반적 이라는 뜻!!
function func<T>(value: T): T { // func<타입 변수 T 선언>(매개변수 value를 타입 변수로 T로): 반환값도 타입 변수 T
return value;
}
let num = func(10);
// num.toUpperCase();
num // 여기서 num은 number로 추론!
if (typeof num === "number") {
num.toFixed();
}
let bool = func(true);
let str = func("string");
let arr = func<[number, number, number]>([1, 2, 3]); // 타입 변수 T에 [number, number, number]가 들어감 -> arr가 [number, number, number]의 튜플 타입으로 추론
// let arr = func([1, 2, 3] as [number, number, number]); 와 같음
// TypeScript에서 길이(length) 속성이 있는 값만 허용하는 함수
function getLength<T extends { length: number }>(data: T) { // 타입 변수 T에 extends를 통해 number 타입의 length 속성이 있는 값만 하용함.
return data.length;
}
- T는 함수 호출 시 자동으로 추론
- 명시적으로 타입을 넘겨주는 것도 가능!
3. 제네릭 함수 활용 사례
1) 두 값을 스왑(swap)하기
// 첫번째 사례
function swap0(a: any, b: any) { // 이 방법은 불안정
return [b, a];
}
function swap<T, U>(a: T, b: U) { // 입력 받은 첫번째 인자와 두번째 인자와 다를 경우에는 이렇게 선언
return [b, a];
}
const [a, b] = swap("1", 2); // swap<string, number>(a: string, b: number): (string | number)[] 로 추론
2) 배열의 첫 번째 값을 반환하기
// 두번째 사례
function returnFirstValue0(data: any){ // 이 방법은 불안정
return data[0]
}
function returnFirstValue1<T>(data: T[]) { // 그냥 data: T 로 할 경우, 아직 모르는 unknown에 data에 배열 data[0]를 쓰지 말라고 에러!!
return data[0];
}
let num = returnFirstValue1([0, 1, 2]); // number 타입으로 정상 추론
// 0
3) 튜플로 첫 번째 타입만 반환하기
function returnFirstValue2<T>(data: [T, ...unknown[]]) { // 배열 타입이 아닌, 튜플 타입으로 작성!! -> 이 경우 str은 number로 추론
return data[0];
}
let str = returnFirstValue2([1, "hello", "mynameis"]); // 이전과 같이 data: T[]만 할 경우 str은 (string | number)[]로 추론 됨.
// -> [1, "hello", "mynameis"] 가 (string | number)[]이기 때문!!
4) 타입 제한 (Constraints)
// 세번째 사례 -> length 속성이 있는 타입만 매개변수로 받아 length를 반환
interface A {
length: number;
}
interface B extends A {} // T extends { length: number } 와 같음
function getLength0(data: any){ // 이 방법은 불안정
return data.length;
}
function getLength<T extends { length: number }>(data: T) { // T의 타입을 number 타입의 length 속성을 가지고 있는 타입을 확장
return data.length;
}
getLength("123");
getLength([1, 2, 3]);
getLength({ length: 1 });
// getLength(undefined);
// getLength(null);
- extends를 사용하여 특정 속성이 있는 타입만 받도록 제한할 수 있음
4. map 함수 직접 구현해보기
// map 메서드
const arr = [1, 2, 3];
const newArr = arr.map((it) => it * 2); // (it) => it * 2 라는 콜백 함수를 arr의 모든 요소에 적용해서 새로운 배열 newArr를 만듬 -> it은 number 타입으로 추론
// [2, 4, 6]
function map0<T>(arr: T[], callback: (item:T) => T){
let result = [];
for (let i = 0; i < arr.length; i++){
result.push(callback(arr[i]));
}
return result;
}
map0(["hi", "hello"], (it) => it.toUpperCase()); // 정상 작동
// map0(["hi", "hello"], (it) => parseInt(it)); // number타입으로 반환된 값을 T 타입인 string에 넣으려해서 에러!!
function map<T, U>(arr: T[], callback: (item: T) => U) { // 2개의 타입 변수 T, U를 사용
let result = [];
for (let i = 0; i < arr.length; i++) {
result.push(callback(arr[i]));
}
return result;
}
map(arr, (it) => it * 2);
map(["hi", "hello"], (it) => parseInt(it)); // 이 경우, T에는 string 타입, U에는 number 타입이 들어가서 정상적으로 작동
5. forEach 함수 직접 구현
// forEach
const arr2 = [1, 2, 3];
arr2.forEach((it) => console.log(it)); // arr의 각 요소에 콜백 함수 (it) => console.log(it) 를 반복 수행
function forEach<T>(arr: T[], callback: (item: T) => void){
for(let i = 0; i < arr.length; i++){
callback(arr[i]);
}
}
forEach(arr2, (it) => { // arr2 가 number 타입이면 it 또한 number 타입이 되어 toFixed를 사용할 수 있어야함. -> it에 number로 정상 추론
console.log(it.toFixed());
});
forEach(["123", "456"], (it) => {
it;
});
6. 제네릭 인터페이스
// 제네릭 인터페이스 -> 제네릭 함수를 만들 때와 유사
interface KeyPair<K, V> {
key: K;
value: V;
}
let keyPair: KeyPair<string, number> = { // 타입으로 정의할 때, KeyPair<string, number>처럼 매개변수의 타입을 선언해야함
key: "key",
value: 0,
};
let keyPair2: KeyPair<boolean, string[]> = {
key: true,
value: ["1"],
};
- 제네릭 인터페이스는 반드시 타입 인자를 명시!!
1) 인덱스 시그니처와 함께
// 인덱스 시그니쳐
interface NumberMap {
[key: string]: number; // key의 타입은 string, value의 타입은 number
}
let numberMap1: NumberMap = {
key: -1231,
key2: 123123,
};
interface Map<V> {
[key: string]: V; // value의 타입을 바꾸어 사용 가능
}
let stringMap: Map<string> = {
key: "value",
};
let booleanMap: Map<boolean> = {
key: true,
};
2) 제네릭 타입 별칭
// 제네릭 타입 별칭
type Map2<V> = {
[key: string]: V;
};
let stringMap2: Map2<string> = {
key: "hello",
};
3) 실용 예제: 학생과 개발자 구분
// 제네릭 인터페이스의 활용 예시
// -> 유저 관리 프로그램
// -> 유저 구분 : 학생 유저 / 개발자 유저
interface Student {
type: "student";
school: string;
}
interface Developer { // student와 developer는 서로 서로소 관계임
type: "developer";
skill: string;
}
interface User<T> {
name: string;
profile: T;
}
interface User0{
name: string;
profile: Student | Developer; // 이 것과 같은 기능
}
function goToSchool0(user: User0) { // 학생 유저만 가능
if(user.profile.type !== "student"){
console.log("잘못 오셨습니다");
return;
}
const school = user.profile.school;
console.log(`${school}로 등교 완료`);
}
function goToSchool(user: User<Student>) { // 학생 유저만 가능 -> 제네릭 인터페이스를 사용함
const school = user.profile.school;
console.log(`${school}로 등교 완료`);
}
// goToSchool(developerUser); developer 타입의 유저는 goToSchool의 함수의 매개변수로 올 수 없음!!
const developerUser: User<Developer> = {
name: "이정환",
profile: {
type: "developer",
skill: "TypeScript",
},
};
const studentUser: User<Student> = {
name: "홍길동",
profile: {
type: "student",
school: "가톨릭대학교",
},
};
- 타입 좁히기 없이 명확한 분리와 제한이 가능해짐
7. 제네릭 클래스
// 제네릭 클래스
class List<T> {
constructor(private list: T[]) {} // 생성자에서 타입을 정의해서 필드를 생략 -> private 타입의 경우에는 this.list = list 또한 생략 가능
push(data: T) {
this.list.push(data);
}
pop() {
return this.list.pop();
}
print() {
console.log(this.list);
}
}
const numberList = new List<number>([1, 2, 3]); // 매개변수로 number가 들어오면 number의 리스트
numberList.pop(); // 3 제거
numberList.push(4); // 4 추가
numberList.print(); // [1, 2, 4]
const stringList = new List<string>(["1", "2"]); // 매개변수로 string이 들어오면 string의 리스트
stringList.push("hello");
stringList.print(); // ['1', '2', 'hello']
8. Promise는 제네릭 클래스
// 프로미스
const promise0 = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve(20); // 비동기 작업의 결과값 20
}, 3000);
});
// promise0.then((response) => { // response의 타입이 unknown이기 때문에 * 10 에서 에레가 발생!!
// console.log(response * 10); // -> 타입 좁히기를 써도 되지만, 번거로움 -> 제네릭 사용!!
// });
const promise = new Promise<number>((resolve, reject) => { // number 타입으로 선언
setTimeout(() => {
// resolve(20); // -> 비동기 작업의 결과값으로 number 만 가능
reject("~~ 때문에 실패");
}, 3000);
});
promise.then((response) => { // 성공할 때
console.log(response * 10); // 20
});
promise.catch((err) => { // 실패할 때 -> 매개변수 err의 타입을 잘 알 수 없음
if (typeof err === "string") {
console.log(err);
}
});
// -> 즉, Promise에서 성공했을 때의 타입은 Promise<number> 에서 정해줄 수 있지만, 실패했을 때의 타입은 전해줄 수 없음
1) Promise를 반환하는 함수
// 프로미스를 반환하는 함수의 타입을 정의
interface Post {
id: number;
title: string;
content: string;
}
function fetchPost(): Promise<Post> { // Promise<Post> 로 함수의 반환 타입을 명시
return new Promise<Post>((resolve, reject) => { // Promise<Post>, <Post> 가 없을 경우에는 unknown으로 추론하기 때문에 이후에 post.id와 같이 속성에 접근 불가
setTimeout(() => {
resolve({
id: 1,
title: "게시글 제목",
content: "게시글 컨텐츠",
});
}, 3000);
});
}
const postRequest = fetchPost();
postRequest.then((post) => {
post.id;
});
9. 깃허브 코드 내용
'TYPESCRIPT' 카테고리의 다른 글
| [TypeScript] TypeScript와 조건부 타입 (0) | 2025.06.30 |
|---|---|
| [TypeScript] TypeScript와 타입 조작 (0) | 2025.06.30 |
| [TypeScript] TypeScript 와 클래스 (1) | 2025.06.28 |
| [TypeScript] TypeScript와 인터페이스 (0) | 2025.06.27 |
| [TypeScript] TypeScript의 함수와 타입 (0) | 2025.06.27 |