Generics

·2022년 5월 24일
0

신기한 개발 세상

목록 보기
7/12
post-thumbnail

Generics vs Any

function funcString(message: string): string {
	return message;
}

function funcNum(message: number): number {
	return message;
}

typescript의 경우 같은 역할을 하지만 타입을 명시해야하다보니 반복적인 함수를 작성하게되는 일이 생길 때가 있다.

보통 이런 경우 Any를 많이 사용하게 된다. 하지만 Any를 사용하게 되면 컴파일할 때 잡아내지 못하는 에러들이 있을 수 있다.

function func(message: any): any {
	return message;
}

console.log(func("Hello").length); // type이 any로 추론
console.log(func(2022).length); // length를 사용할 수 없지만 컴파일 시 문제가 생기지 않음

이런 문제를 해결하기 위해 Generic을 사용한다.

function generics<T>(message: T): T {
	return message;
}

console.log(generics("Hello")); // type이 string의 하위 타입인 literal 타입으로 지정된다.
console.log(generics("Hello").length); // number로 타입 추론이 된다.
console.log(generics(2022).length); // number에선 length를 사용할 수 없기 때문에 에러가 뜬다.

GenericAny와 다르게 들어오는 input에 따라 타입으로 된 연산이 가능하게 된다.

Generics 기본 사용법

Generic은 타입을 직접 지정할 수도 있고 지정하지 않은 경우엔 input을 가지고 타입을 추론한다.

function func<T>(message: T): T {
	return message;
}

const a = func<string>('string'); // 타입을 직접 지정
const b = func(36); // 타입을 자동으로 추론
function func<T>(message: T, comment: U): T {
	return message;
} // 타입을 여러게 넣는것도 가능하다

const a = func<string, number>('string', 2022); // 타입을 직접 지정
const b = func(2022, 5); // 타입을 자동으로 추론

Generics Array & Tuple

배열

function funcArr<T>(message: T[]): T {
	return message[0];
}

funcArr(["hello", "world"]); //  T가 string으로 추론
funcArr(["hello", 5]); 
/** 
  T가 string | number 로 추론 
  => 둘 다 사용할 수 있는 method들만 사용할 수 있게 된다.  
**/

튜플

function funcTuple<T, K>(message: [T, K]): T {
	return message[0];
}

funcTuple(["hello", "world"]); // T가 string으로 추론
funcTuple(["hello", 5]); 
/** 
  T가 string으로 추론
  => string에서 사용할 수 있는 method를 다 사용할 수 있게 된다.
**/

Generics Function

type alias

type genericFunction = <T>(message: T) => T;

const func: genericFunction = <T>(message: T): T  => {
  return message;
}

interface

interface genericFunction { 
  <T>(message: T): T;
}

const func: genericFunction = <T>(message: T): T  => {
  return message;
}

Generics Class

class Person {
  private _name: T;
  
  constructor(name: T) {
    this._name = name;
  }
}

위 예시에서 T의 유효범위는 class 전체에서 발생하게 된다. 그래서 class에도 명시해줘야한다.

class Person<T> {
  private _name: T;
  
  constructor(name: T) {
    this._name = name;
  }
}

new Person("name"); // 타입 추론
new Person<string>("name"); // 타입 명시

함수와 동일하게 여러 타입도 지정 가능하다.

class Person<T, K> {
  private _name: T;
  private _age: K;
  
  constructor(name: T, age: K) {
    this._name = name;
    this._age = age;
  }
}
new Person("name", 2022);

Generics with extends

generic에서도 extends가 사용될 수 있다.

class PersonExtends<T extends string | number> {
  private _name: T;
  
  constructor(name: T) {
    this._name = name;
  }
}

new PersonExtends("Name");
new PersonExtends(2022);
new PersonExtends(true); // Error!

<T extends string | number>는 string과 number를 상속한다는 의미가 아니고 (사실 둘을 상속한다는 말이 이상하기도 하다.) string이나 number만 유효하다는 의미이다. 다른 타입의 input에는 에러를 뱉어낸다.

이런 제약으로 인해 다른 개발자나 내일의 나에게 좋은 지침서가 되어준다.

Generic과 keyof 활용해 정확히 타이핑하기

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

const person: IPerson {
  name: "name",
  age: 2022,
};
  
function getProp(obj: IPerson, key: "name" | "age"): string | number {
  return obj[key];
};
  
getProp(person, "address"); // Error
  
function setProp(obj: IPerson, key: "name" | "age", value: string | number ): void {
  obj[key] = value;
}
  
setProp(person, "address", "서울"); // Error
setProp(person, "name", 2022); // Error가 필요한 상황이지만 Error가 나지 않는다.

value를 union type을 이용해 작성하게 되면 value의 타입이 name일때 string을 age일 땐 number여야한다는걸 표현할 수 없다. getProps의 return type도 마찬가지이다.

이 문제를 해결하기 위해선 keyofgeneric을 활용하면 된다.

keyof만 활용하면

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

const person: IPerson {
  name: "name",
  age: 2022,
};

/**
IPerson[keyof IPerson] 
=> IPerson["name" | "age"]
=> IPerson["name"] | IPerson["age"]
=> string | number
**/
function getProp(obj: IPerson, key: keyof IPerson): IPerson[keyof IPerson] {
  return obj[key];
};

getProp(person, "name"); // string | number 
getProp(person, "address"); // Error
  
function setProp(obj: IPerson, key: keyof IPerson, value: string | number ): void {
  obj[key] = value;
}

setProp(person, "address", "서울"); // Error
setProp(person, "name", 2022); // Error가 필요한 상황이지만 Error가 나지 않는다.
  

주석을 보면 결국엔 union type으로 정의되는거라 여전히 동일한 문제가 있다.

여기에 generic을 추가해주면 key와 value 타입적인 관계를 정확하게 매핑해줄 수 있게된다.

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

const person: IPerson {
  name: "name",
  age: 2022,
};

/**
K는 keyof T로 제한된 형태이다. 즉, K는 name 이라는 literal type 혹은 age라는 literal type으로 지정된다.
그래서 T[K]는
=> K가 name이면 IPerson["name"]: string
=> K가 age면 IPerson["age"]: number
이렇게 타입이 지정된다.
**/
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
};
  
getProp(person, 'name'); //string
  
function setProp<T, K extends keyof T>(obj: T, key: K, value: T[K] ): void {
  obj[key] = value;
}
setProp(person, "name", 39); //Error!!
setProp(person, "name", "Anna");
profile
이제는 병아리는 벗어나야하는 프론트개발자

0개의 댓글