Generics

sangjuneeeee·2024년 5월 6일

TypeScript

목록 보기
6/9

Built-in Generics

const names: Array<string> = [];    // string[]과 동일, Array<T>
const names2: Array<string | number> = [];    // (string | number)[]
let promise: Promise<string> = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Promise resolved!");  // 1초 후에 Promise를 Fulfilled 상태로 변경
  }, 1000);
});

promise.then((message) => {
  console.log(message);  // "Promise resolved!"
  
}).catch((error) => {
  console.log("Promise rejected with error: " + error);
});

Promise는 JavaScript와 TypeScript에서 비동기 작업을 처리하기 위한 객체입니다. Promise는 비동기 작업의 최종 완료(또는 실패)와 그 결과 값을 나타냅니다.

Promise는 다음 세 가지 상태 중 하나를 가질 수 있습니다:

  1. Pending(대기 중): 비동기 처리가 아직 완료되지 않은 상태입니다.
  2. Fulfilled(이행됨): 비동기 처리가 성공적으로 완료된 상태입니다.
  3. Rejected(거부됨): 비동기 처리가 실패하거나 오류가 발생한 상태입니다.
  • 왜 제네릭을 쓰는가?
let promise: Promise<number> = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Promise resolved!");
  }, 1000);
});

promise.then((data) => {
  data.split(' ');    // ERROR, 반환값에 대한 타입이 TypeScript의 지원을 받을 수 있게 됨
}).catch((error) => {
  console.log("Promise rejected with error: " + error);
});

split()은 string에서만 가능.

  • 명시의 차이
let promise = new Promise<string>((resolve, reject) => {...});
// Promise가 처리하는 값이 문자열이라는 것을 TypeScript가 알 수 있음
let promise: Promise<string> = new Promise((resolve, reject) => {...});
//변수 `promise`가 문자열을 처리하는 Promise 객체를 저장해야 한다는 것을 TypeScript가 알 수 있음

둘 다 동일한 작업을 함, 코드 선호도에 따라 결정.

Create a Generic Functions

function merge(objA: object, objB: object) {
	return Object.assign(objA, objB);    // assgin()은 객체를 합치는 메서드
}
const mergedObj = merge({name: 'Max'}, {age: 30});
console.log(mergedObj.age);    // ERROR, 객체는 합쳐졌지만 TypeScript는 알 수 없음.

object에 어떤 타입이, 얼마나 들어갈지 모르기 때문에 문제가 생김.

function merge<T, U>(objA: T, objB: U) {
	return Object.assign(objA, objB);
}
const mergedObj = merge({name: 'Max', hobbies: ['Sports']}, {age: 30});
console.log(mergedObj.age);    // 30

제네릭 함수는 동적으로 설정 됨

const mergedObj = merge({name: 'Max', hobbies: ['Sports']}, {age: 30});

const mergedObj = merge<{name:string, hobbies: string[]}, {age: number}> 
						{name: 'Max', hobbies: ['Sports']}, {age: 30});

제네릭 함수를 사용하게 되면 위와 같은 효과를 나타나게 됨. 실제로 아래 코드도 잘 작동하게 됨.

  • 위 예제의 내용을 실제로 시도해보면 에러 발생 됨

Working with Constraints

Type Guard로 인한 에러, extends로 제약을 걸어 해결

function merge<T extends object, U extends object>(objA: T, objB: U): T & U {
  return Object.assign({}, objA, objB);
  // Object.assign(objA, objB);는 objA에 복사
  // Object.assign({}, objA, objB);는 새로운 객체에 복사
}
const mergedObj = merge({name: 'Max', hobbies: ['Sports']}, {age: 30});
const mergedObj1 = merge({name: 'Max', hobbies: ['Sports']}, 30});    // ERROR

T & U를 사용하는 이유는, 입력으로 받은 두 객체 objAobjB의 모든 속성을 포함하는 새로운 객체를 반환하기 때문입니다.

예를 들어, T{ name: string; }이고 U{ age: number; }라면, T & U{ name: string; age: number; }가 됩니다. 따라서 merge 함수는 name 속성과 age 속성을 모두 가진 객체를 반환합니다.

Another Generic Function

interface Lengthy {
	length: number;
}

function countAndDescribe<T extends Lengthy>(element: T): [T, string] {
	let descriptionText = 'Got no value.';
	if (element.length === 1) {
		descriptionText = 'Got 1 element.';
	} else if ( element.length > 1) {
		descriptionText = 'Got ' + element.length + ' elements.';
	}
	return [element, descriptionText];
}
console.log(countAndDescribe('Hi there!')); 
// [ 'Hi there!', 'Got 9 elements.' ]
console.log(countAndDescribe(['Sports', 'Cooking'])); 
// [ [ 'Sports', 'Cooking' ], 'Got 2 elements.' ]
console.log(countAndDescribe(10));    // ERROR

Lengthy 인터페이스의 length 속성은 element가 문자열이나 배열과 같이 length 속성을 가진 타입일 경우 그 length 속성에 접근하기 위해 사용됩니다.

(element: T)를 통해 매개변수의 입력 타입을 결정할 수 있음.

keyof

function extractAndConvert(obj: object, key: string) {
	return 'Value: ' + obj[key];    // ERROR, obj에 key가 있을 보장이 없기 때문.
}

extractAndConvert({}, 'name');



function extractAndConvert<T extends object, U extends keyof T>(obj: T, key: U) {
	return 'Value: ' + obj[key];
}

extractAndConvert({}, 'name');    // ERROR, 객체에 'name' 키가 없기 떄문
extractAndConvert({name: 'Max'}, 'name');
extractAndConvert({name: 'Max'}, 'age');    // ERROR, 객체에 'age' 키가 없기 떄문

위처럼 Generic 타입과 keyof를 사용하면 예외적인 상황이나 개발자의 실수를 사전에 미리 방지할 수 있게 됨

  1. 객체에 keyof 적용하기
type User = {
  name: string;
  age: number;
};

type UserKeys = keyof User; // "name" | "age"
  1. 인터페이스에 keyof 적용하기
interface Product {
  id: number;
  name: string;
  price: number;
}

type ProductKeys = keyof Product; // "id" | "name" | "price"
  1. 클래스에 keyof 적용하기
class Car {
  model: string;
  year: number;
}

type CarKeys = keyof Car; // "model" | "year"
  1. 배열에 keyof 적용하기
type ArrayKeys = keyof Array<string>; // number | "length" | "toString" | "pop" | "push" | "concat" | "join" | ...
  1. 함수에 keyof를 적용하려고 하면 함수의 속성에 대한 키를 얻습니다
type FunctionKeys = keyof Function; // "apply" | "call" | "bind" | "prototype" | "length" | "name" | ...

typeof와 keyof의 차이점

  1. typeoftypeof 연산자는 변수의 타입을 추출합니다. JavaScript에서 typeof는 실행 시간에 변수의 타입을 반환하지만, TypeScript에서는 컴파일 시간에 변수의 타입을 추출합니다. 따라서 TypeScript에서 typeof는 변수, 객체, 배열 등의 타입을 가져오는 데 사용됩니다.
let num = 123;
type NumType = typeof num; // number
  1. keyofkeyof 연산자는 객체의 속성 키를 추출합니다. 이는 주로 객체의 속성 이름을 타입으로 만드는 데 사용됩니다. keyof를 사용하면 객체의 속성 이름을 문자열 또는 심볼 타입으로 추출할 수 있습니다.
type Person = {
  name: string;
  age: number;
};
type PersonKeys = keyof Person; // "name" | "age"

Generice Classes

class DataStorage<T> {
	private data: T[] = [];
	
	addItem(item: T) {
		this.data.push(item);
	}

	removeItem(item: T) {
        this.data.splice(this.data.indexOf(item), 1);
    }

    getItems() {
        return [...this.data];
    }
}

const stringStorage = new DataStorage<string>();
stringStorage.addItem('asdf');
stringStorage.addItem('qwer');
stringStorage.removeItem('asdf');
console.log(stringStorage.getItems());    // [ 'qwer' ]

const numberStorage = new DataStorage<number>();
numberStorage.addItem(12);
numberStorage.addItem(34);
numberStorage.removeItem(12);
console.log(numberStorage.getItems());    // [ 34 ]

여기서 문제는 this.data.splice(this.data.indexOf(item), 1); 이 방식을 제거하는 방법은
string | number | boolean에서만 가능하며 object 타입으로 진행하거나 값을 못 찾는 경우에는 -1을 리턴하여 맨 마지막 요소를 제거 하기 때문에 문제가 있음 (메모리 접근)
이 문제는 TypeScript와는 별개이므로 설명하지 않겠음.

Generice Utility Types

1. Pick(Type, Keys)

Pick 타입은 기존에 있던 타입에서 필요한 것만 골라서 새로운 타입을 만들어주는 건데요.

Pick 유틸리티 타입은 우리가 고른 거 외에는 전부 제거합니다.

type Student = {
  name: string
  lastName: string
  age: number
  class: string
}

type SomeStudent = Pick<Student, "name" | "age">

// type SomeStudent = {
//  name: string;
//  age: number;
// }

2. Omit(Type, Keys)

Omit 유틸리티 타입은 Pick 유틸리티 타입과 정반대로 작동됩니다.

삭제할 Keys 항목만 넣으면 그거 말로 나머지를 리턴해 주는 타입입니다.

기존 타입에서 일부만 제거하려고 할 때 아주 유용합니다.

type Student = {
  name: string
  lastName: string
  age: number
  class: string
}

type SomeStudent = Omit<Student, "lastName" | "class">

// type SomeStudent = {
//  name: string;
//  age: number;
// }

3. Readonly(Type)

Readonly 유틸리티 타입은 작성된 값이 변경할 수 없게 만들어 줍니다.

Readonly로 새롭게 생긴 타입에 새로운 값을 할당하려고 하면 타입스크립트 경고가 나옵니다.

type Student = {
  name: string,
}

type ReadOnlyStudent = Readonly<Student>

const student: ReadOnlyStudent = {
  name: 'Jin',
}

student.name = 'Jung'
// Cannot assign to 'name' because it is a read-only property.

4. Partial(Type)

네번 째인 Partial 유틸리티 타입은 기존 타입의 항목을 전부 옵셔널(optional)로 만들어 줍니다.

이 유틸리티 타입은 우리가 받은 객체가 아직 뭔지 모를 때 아주 유용합니다.

type Student = {
  name: string
  lastName: string
  age: number
  class: string
}

type PartialStudent = Partial<Student>

// type PartialStudent = {
//   name?: string | undefined;
//   lastName?: string | undefined;
//   age?: number | undefined;
//   class?: string | undefined;
// }

5. Required(Type)

Required 타입은 Partial 유틸리티 타입과 정반대로 작동합니다.

모든 옵셔널 상태인 항목에 대해 옵셔널 상태를 없애주거든요.

type Student = {
  name?: string
  lastName?: string
  age?: number
  class?: string
}

type RequiredStudent = Required<Student>

// type RequiredStudent = {
//   name: string;
//   lastName: string;
//   age: number;
//   class: string;
// }

6. Record(Type)

TypeScript의 Record 유틸리티 타입은 객체의 키와 값에 대한 타입을 지정하는데 사용됩니다. Record는 두 개의 타입 매개변수를 받으며, 첫 번째 매개변수는 키의 타입, 두 번째 매개변수는 값의 타입을 나타냅니다.

Record의 기본 형태는 다음과 같습니다:

type Record<K extends keyof any, T> = {
	[P in K]: T;
};

예를 들어, Record<string, number>는 모든 키가 문자열이고 모든 값이 숫자인 객체의 타입을 나타냅니다:

const obj: Record<string, number> = {
	prop1: 1,
	prop2: 2,
	prop3: 3,
	// prop4: "four" // 이 줄은 오류를 발생시킵니다. 값이 숫자가 아닙니다.
};

또한, Record는 열거 가능한 키의 집합을 사용하여 객체의 타입을 지정하는 데도 사용될 수 있습니다:

type RGB = "red" | "green" | "blue";
const colors: Record<RGB, string> = {
	red: "#FF0000",
	green: "#00FF00",
	blue: "#0000FF",
};

Generic Types vs Union Types

  • Generice Types
class DataStorage<T> {
	private data: T[] = [];
	
	addItem(item: T) {
		this.data.push(item);
	}

	removeItem(item: T) {
        this.data.splice(this.data.indexOf(item), 1);
    }

    getItems() {
        return [...this.data];
    }
}
  • Union Types
class DataStorage {
	private data: string[] | number[] | boolean[] = [];
	
	addItem(item: string | number | boolean) {
		this.data.push(item);    // ERROR
	}

	removeItem(item: string | number | boolean) {
        this.data.splice(this.data.indexOf(item), 1);    // ERROR

    getItems() {
        return [...this.data];
    }
}

제네릭 타입과 유니온 타입과 유사하게 허용된 타입을 설정해줄 수 있지만,

제네릭은 동적으로 설정되기에 내부 함수에 대해서 제한을 둘 수 있고,
유니온 함수는 추가적인 작업을 필요로 할 수 있다.

profile
지식 쌓아두기 블로그

0개의 댓글