76일차

JiHun·2023년 7월 28일

부트캠프

목록 보기
53/56
post-thumbnail

제네릭

제네릭은 코드 재사용성을 높이는 장치다.

제네릭이 왜 필요한가?

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값만 들어갈 수 있다.

profile
안녕하세요. 프론트엔드 개발자 송지훈입니다.

0개의 댓글