
제네릭은 코드 재사용성을 높이는 장치다.
function identity(arg: string): stirng {
return arg;
}
이건 string 타입만 설정한 걸 볼 수 있다. 하지만 number 타입이 들어올 수도 있다면?
function identity(arg: string | number) {
return arg;
}
이렇게 |연산자를 사용해서 유니온 타입으로 나타내 줄수도 있지만, 여러 타입이 들어온다고 한다면 모든 타입을 다 적어줘야 할까?
그렇다고 any로 작성한다면?
function identity(arg: any): any {
return arg;
}
모든 타입이 파라미터로 작성될 수 있고, 리턴값이 모든 타입으로 나올 수 있기 때문에 타입스크립트의 장점인 타입 검사를 사용하지 못하고, 타입 안정성을 보장하지 못한다.
프로그래밍하는 사람들은 코드가 길어지는 것을 못 참는 거 같다.
function identity<T>(arg: T): T {
return arg;
}
<T>는 파라미터의 타입을 캡처하고, 이 정도를 나중에 사용할 수 있게 한다.
타입 불문 하고 다 동작한다.
그래서 이 함수를 호출할 때, 정확한 타입을 적어 줄 수 있다.
const str = identity<string>("hi");
인터페이스에도 제네릭을 사용할 수 있다.
interface Pair<T> {
first: T;
seconde: T;
}
const numberPair: Pair<number> = { first: 10, second: 20};
console.log(numberPair); // { first: 10, second: 20};
const stringPair: Pair<string> = { first: 'hello', second: 'world' };
console.log(stringPair); // { first: "hello", second: "world" }
만약 제네릭을 안쓰면, 이런 식으로 타입을 써주는 것랑 같은 것이다
interface PairNumber {
first: number;
second: number;
}
interface PairString {
first: string;
second: string;
}
모든 타입에 대응해서 interface를 만든다면, 코드가 비슷하지만 다른 것들이 굉장히 많이 생기게 된다.
class Box<T> {
data: T;
constructor(initialData: T) {
this.data = initialData;
}
getData(): T {
return this.data
}
setData(newData: T): void {
this.data = newData;
}
}
const numberBox = new Box<number>(42);
numberBox.setData(99);
const stringBox = new Box<string>("hello");
function printLength<T>(text: T): T {
console.log(text.length);
return text;
}
text를 제네릭 타입화시키면, T에는 모든 타입이 들어가기 때문에
문자열, 배열 등에만 .length가 동작하므로, 제네릭 타입 T가 .length프로퍼티를 가지고 있을 것으로 가정할 수 없다.
따라서, T가 .length 프로퍼티를 가지고 있을지 여부를 미리 알 수 없으므로 TypeScript는 이를 허용하지 않는다.
Properth 'length' does not exist on type 'T'.
이러한 에러를 띄운다.
function printLength<T>(text: Array<T>): Array<T> {
console.log(text.length);
return text;
}
제네릭 함수에 어느 정도 어떤 타입이 들어올 것인지 힌트를 줄 수 있다.
function printLength<T>(text: T): T {
console.log(text.length);
return text;
}
T로 그냥 적게 되면 아무 타입이나 들어와서 .length 프로퍼티를 보고 TypeScript에서 에러를 발생시킨다.
interface TextLength {
length: number
}
function printLength<T extends TextLength>(text: T): T {
console.log(text.length);
return text;
}
제약 조건을 걸어서 length에 제약조건을 걸 수 있다.
length 프로퍼티를 가진 타입에 대해서만 동작하며, 그렇지 않은 타입의 값을 전달하면 TypeScript가 오류를 표시한다.
printLength("Hello, TypeScript!"); // => 18
printLength([1, 2, 3, 4, 5]); // => 5
printLength(42); // 오류: number 타입은 .length 프로퍼티를 가지고 있지 않음
print(true); // 오류: boolean 타입은 .length 프로퍼티를 가지고 있지 않음
또한, keyof를 이용해서 제약을 줄 수도 있다.
interface Person {
name: string;
age: number;
address: string;
}
function getProperty<T extends keyof Person>(person: Person, property: T): Person[T] {
return person[property];
}
const person: Person = {
name: "Alice",
age: 30,
address: "Wonderland",
};
const nameValue: string = getProperty(person, "name");
const ageValue: number = getProperty(person, "age");
const addressValue: string = getProperty(person, "address");
console.log(nameValue); // => "Alice"
console.log(ageValue); // => 30
console.log(addressValue); // => 출력: "Wonderland"
keyof는 그냥 extends만 적는거랑 다르게 동작한다. T에는 interface에 Key값만 들어갈 수 있다.