Generic

홍범선·2023년 10월 29일
0

타입스크립트

목록 보기
24/34

제너릭 함수에서 사용하기

function whatValue(value: any){
  return value;
}

const value = whatValue('test'); //value는 any타입이 된다.

any타입으로 받으면 value는 any타입이 된다.

타입의 선언을 함수 만들 때 선언하지 말고 실행할 때 선언하면 좋겠다.

function genericWhatValue<T>(value: T):T{
  return value;
} 
// T라는 제너릭을 사용을 하고 value는 T타입고 반환타입도 T타입이다.

const genericResult = genericWhatValue<string>('123')
//입력을 string타입으로 입력해야지 에러가 안남
// 제너릭을 쓰면 이 타입의 선언은 실제 실행할 때로 미룰 수 있다.

const genericResult2 = genericWhatValue('123'); 
//T타입이 '123'으로 되기 때문에 '123'타입으로 유추가 된다. const를 써서 정확한 타입으로 유추한다.

let genericResult3 = genericWhatValue('123');
//string타입으로 유추

<>를 사용하면 함수를 실행할 때 파라미터를 넣어주는 것 처럼 type을 넣어줄 수 가 있다.
정확히 <>안에 type을 넣어주면 좋지만 안 넣어줘도 자동으로 유추를 하게 된다.
const는 입력한 값 자체가 타입이 되고
let에는 해당 타입을 유추한 타입이 된다.

MultipleGeneric


function multipleGeneric<X, Y, Z>(x: X, y:Y, z:Z){
  return {
    x,
    y,
    z
  }

}

const multipleGenericResult = multipleGeneric<number, boolean, string> (
  123,
  true,
  '123'
) // 타입 순서대로, 정확히 입력해야지 에러가 나지 않는다.

const multipleGenericResult2= multipleGeneric (
  123,
  true,
  '123'
) // 제너릭을 입력하지 않아도 자동으로 유추된다.

제너릭은 여러개의 타입을 지정할 수 있다.
마찬가지로 <>안에 타입을 정의하지 않아도 자동으로 유추된다.
const지만 객체이기 때문에 각 타입에 대해 유추가 된다.

튜플

function getTuple<X, Y>(val1 : X, val2:Y){
  return [val1, val2] as const;
}

const tuple = getTuple(true, 100);

튜플도 마찬가지로 <>안에 타입을 지정안해도 유추가 된다.
as const를 하게 되면 readonly가 ㄷ ㅚㄴ다.

생성자

class Idol {
  name: string;
  age: number;

  constructor(name: string, age:number){
    this.name = name;
    this.age = age;
  }
}

class Car{
  brand:string;
  codeName: string;

  constructor(brand:string, codeName:string){
    this.brand = brand;
    this.codeName = codeName
  }
}

function instantiator<T extends {new (...args: any[]) : {}}>(constructor: T,
  ...args: any[]){ 
  // T타입은 옆에 형태를 상속을 받을 것이다. new니까 constructor인데 constructor에서 타입을 무한히 받을 것이고 실행하면 객체타입이 반환된다.
    return new constructor(...args)
}

console.log(instantiator(Idol, '아이유', 23));
console.log(instantiator(Car, 'BMW', 22223));

생성자도 제너릭으로 만들 수 있다.

인터페이스에서 제너릭사용하기

언제 사용할까?
어떤 데이터든 들어가긴 하고 싶은데 타입체크를 받고 싶다 하면 이때 제너릭을 사용하면 좋다.

interface Cache<T>{
  data: T[];  
  //어떤 데이터든 들어가긴 하고 싶은데 타입체크를 받고 싶다 하면 이때 제너릭을 사용하면 좋다.
  lastUpdate: Date;
}

const cache1: Cache<string> = {
  data:['data1', 'data2'], 
  //만약 data:['data', 'data2', 123]을 사용하면 에러가 난다. string으로 구성된 배열만 가능하다.
  lastUpdate: new Date()
}

const cache2: Cache<number> = {
  data: [123, 345],
  lastUpdate: new Date()
} //함수에선 제너릭 생략해도 유추가 됬지만 인터페이스는 안된다. 

인터페이스에서 제너릭을 생략하면 유추가 되지 않고 에러가 발생한다.
하지만 인터페이스는 Default값을 지정가능하다.
Default값을 지정하면 제너릭을 생략해도 Default로 되기 때문에 에러가 발생하지 않는다.


interface DefaultGeneric<T = string>{
  data: T[];

}

const cache3: DefaultGeneric = {
  data:['123','45'],
} // 제너릭에서 default값을 사용가능해서 <>없어도 string으로 된다.

const cache4: DefaultGeneric<number> = {
  data:[123,456]
} // 제너릭에서 default값 이외에도 된다.

타입에서 제너릭 사용하기

type GenericSimpleType<T> = T;

const genericString: GenericSimpleType<string> = '코드팩토리'; //string말고 number타입을 넣으면 에러발생

// const genericString2: GenericSimpleType = '코드팩토리' //자동유추 안된다.

타입에서도 제너릭을 사용할 수 있다.
타입에서 자동 유추는 되지 않는다.

interface DoneState<T>{
  data: T[];
}

interface LoadingState{
  requestedAt: Date;
}

interface ErrorState{
  error:string;
}

type State = DoneState<string> | LoadingState | ErrorState; //이렇게해도되지만 State타입을 지정을 할 때 DoneState도 지정해주고 싶다

이렇게해도되지만 State타입을 지정을 할 때 DoneState도 지정해주고 싶다면?

type State1<T> = DoneState<T> | LoadingState | ErrorState; 
//이렇게하면 state1에 넣어준 T타입이 DoneState에도 T타입으로 들어간다.

타입은 자동 유추가 안된다. 하지만 Default를 사용하면 제너릭을 사용하지 않아도 Default값으로 된다.

type State2<T = string> = DoneState<T> | LoadingState | ErrorState; //이렇게하면 Default값이 지정된다.
//이렇게하면 state1에 넣어준 T타입이 DoneState에도 T타입으로 들어간다.

클래스에서 제너릭 사용하기

클래스에서 제너릭을 사용하는 경우는 굉장히 많다.

class Pagination<Data, Message> {
  data: Data[] = [];
  message?: Message
  lastFetchedAt?: Date
}

const pgData = new Pagination<number, string>();

pgData.data; // number[]
pgData.message; //  string | undefined
pgData.lastFetchedAt // Date | undefined

직접 클래스 내 필드에 입력받은 제너릭으로 선언할 수 있다.

물론 constructor에서도 가능하다.

class Pagination2<Data, Message> {
  data: Data[] = [];
  message?: Message;
  lastFetchedAt?: Date;

  constructor(data: Data[], message? : Message, lastFetchedAt? : Date){
    this.data = data;
    this.message = message;
    this.lastFetchedAt = lastFetchedAt;
  }//클래스에서 선언할 때 선언해 놓은 이 generic type들을 constructor 파라미터에서도 사용가능하다.
}

const pagination2 = new Pagination2<number, string>([123, 456]); //<number,string>이 없어도 되지만 명시해주는 것이 좋다.

pagination2.data; // 입력한 값으로부터 유추해서 number[]
pagination2.message; //unknown => 제너릭으로 넣어주지 않아서 알 수 없음
pagination2.lastFetchedAt; //Date | undefined

클래스의 인스턴스를 생성할 때 <> 제너릭을 사용안하여도 유추가 되지만 사용하는 것이 좋다.
사용하지 않으면 유추가 될 때 unknown이 될 수 도 있다.

class DefaultGeneric<T = boolean>{
  data : T[] = [];
}

const DefaultGeneric2 = new DefaultGeneric();
DefaultGeneric2.data; // boolean

클래스에서도 제너릭은 Default를 사용할 수 있다.

상속에서 제너릭 사용하기

class BaseCache<T>{
  data: T[] = [];
}

class StringCache extends BaseCache<string>{
  
}

const stringCache = new StringCache();
stringCache.data; // string[]

상속을 받을 때 parent쪽에 제너릭을 선언해주는 방법이다.

class GenericChild<T, Message> extends BaseCache<T>{
  message?: Message;

  constructor(message?: Message){
    super();
    this.message = message;
  }
} // child에서 선언한 T값을 Parent인 BaseCache에 T를 그대로 넘겨주겠다. 라는 의미

const genericChild = new GenericChild<string, string>('error');
genericChild.data; //string[]
genericChild.message // string | undefined 옵셔널이니까

child에서 선언한 T값을 Parent인 BaseCache에 T를 그대로 넘겨주겠다. 라는 의미이다.
T => string
Message => string이 들어가고
message에는 'error'가 들어간다.
BashCache에도 string이 들어가서
data는 string[]이 되고 message string|undefined가 된다. 옵셔널이기 때문

제너릭의 상속

class Idol<T>{
 information: T;

 constructor(information: T){
   this.information = information;
 }
}

물론 이렇게 할 수 있다.
하지만 아무 타입이나 들어가게 하고 싶진 않고 특정 요소를 무조건 포함한 타입으로 하고 싶다면 다음과 같이하면 된다.
(name 프로퍼티가 존재했으면 좋겠다.)

interface BaseGeneric{
 name: string;
}

class Idol2<T extends BaseGeneric>{ //BaseGeneric이 제공해주는 형태를 T는 무조건 따라줘야 한다.
 information: T;

 constructor(information: T){
   this.information = information;
 }
}

const yuJin = new Idol2({
 name: '안유진',
 age:23,
}) 

이렇게 하게 되면 T는 반드시 BaseGeneric이 제공해주는 형태로 강제할 수 있다.
그래서 만약 age를 지우면 에러가 안나지만 name을 지우면 에러가난다. 제너릭을 extend하면 우리가 extend한 이 구조를 강제할 수 있어서 name이란 프로퍼티를 필수적으로 넣어줘야 한다.

keyof와 함께 사용하기

const testObj = {
  a:1,
  b:2,
  c:3,
}

function objectParser<o>(obj : o, key:string){
   return obj[key]; //에러가 발생한다. obj는 o타입이 어떤 타입인지 알 수 없다.
 }

에러가 발생한다.

//key에다가 object 키값을 무조건 넣게 될 것이라고 명시를 해주어야 함
// testObj를 obj에 넣었을 때 key는 a,b,c만 되어야 한다.
// keyof o를 하게 되면 o의 key값을 가져올 수 있다.
// extends를 하면 K는 o의 객체의 키값들을 포함하는 구조로 강제할 것이다.
function objectParser2<o,K extends keyof o>(obj:o, key:K){
  return obj[key];
}

const val =  objectParser2(testObj, 'a'); //d는 에러

Ternary 제너릭

class Idol3{
  type?: string;
}

class FemaleIdol extends Idol3{
  type:'Female Idol' = 'Female Idol';
}

class MaleIdol extends Idol3{
  type : 'Male Idol' = 'Male Idol';
}

type specificIdol<T extends Idol3> = T extends MaleIdol ? MaleIdol : FemaleIdol;

const idol2: specificIdol<FemaleIdol> = new FemaleIdol() 
// female idol로 나옴 왜냐하면 femaleIdol idol3를 extends하니까 제너릭이 될 수 있다. 
//또 T 타입은 MaleIdol인지 테스트를 한다.

specificIdol은 T타입을 제너릭으로 받는데 무조건 Idol3를 extends(형태를 강제적으로 따라줘야 함 초과 가능)를 해야하고 T가 만약에 MaleIdol을 extend하면은 Male타입 아니면 Female타입

Method에서 제너릭 사용하기

class Idol <T>{
  id: T;
  name: string;

  constructor(id : T, name: string){
    this.id = id;
    this.name = name;
  }

  sayHello<Time>(logTime: Time){
    return `[${logTime}] 안녕하세요 제 이름은 ${this.name}이고 ID는 ${this.id}입니다.`
  }
}

const yuJin = new Idol('a999', '안유진'); 
// 굳이 <>직접 선언안해도 <string, number>로 유추된다.
console.log(yuJin.sayHello('2023'));
//마찬가지로 sayHello<string>을 하지 않아도 string으로 유추된다.
console.log(yuJin.sayHello<number>(1992));

class Message<T>{
  sayHello<Time>(logTime: Time, message:T){
    console.log(`log Time ${typeof logTime} / message: ${typeof message}`);
  }
}

const message = new Message<string>();
//클래스 재너릭에 string을 직접 넣어주는 것도 가능하다.
message.sayHello<number>(2000, '하이!'); 
//logtime은 number, message는 T타입(string)이 되기 때문에 number,string타입이외에는 에러가 난다.

class DuplicatedGenericName<T>{
  sayHello<T>(logTime : T){
    console.log(`logTime: ${typeof logTime}`);
  }
} //클래스 제너릭 T와 메서드 제너릭 T와 같을 때 메서드 제너릭 T가 우선시 된다.

const duplicate = new DuplicatedGenericName<string>();
duplicate.sayHello<number>(123);
// duplicate.sayHello<number>('23'); 스트링 사용하면 안된다.

여기서 중요한 것은 클래스에서 제너릭을 함수로 넘길 수 있고
만약 클래스와 함수 제너릭이 같을 때에는 메서드 제너릭이 우선된다.

implements에서 제너릭 사용하기

interface Singer <T, V>{
  name: T;
  sing(year: V):void;
}

class Idol implements Singer<string, number>{
  name: string;

  constructor(name: string){
    this.name = name;
  }
  sing(year: number): void {
    console.log(`[${year}] ${this.name}이 노래를 부릅니다.`)
  }
}

const yuJin = new Idol('안유진');
yuJin.sing(2003);

Idol클래스에 Singer 인터페이스를 implements 하고 있다. 이 때 클래스를 선언과 동시에 Singer인터페이스 제너릭에 <string, number>를 넣어주고 있다.
그래서 Idol클래스는 name프로퍼티가 string이여야 하고
sing메소드는 파라미터가 year에 number타입이여야 하고 반환은 void여야 한다.
이것을 idol클래스에 구현해야 한다.
idol클래스에서 name과 sing이 인터페이스에 제러릭으로 선언된 타입과 일치하지 않으면 에러가 발생한다.

class Idol2 <T, V> implements Singer<T, V> {
  name: T;

  constructor(name: T){
    this.name = name;
  }
  sing(year: V): void {
    console.log(`[${year}] ${this.name}이 노래를 부릅니다.`)
  }
}

const wonYoung = new Idol2<string, number>('장원영');
wonYoung.sing(2003);

기존에는 클래스선언과 동시에 제너릭을 넣어줬다. 하지만 이번에는 인스턴스 생성할 때 재너릭을 주입해줄 수 있다.

Promise에서 제너릭 사용하기

const afterTwoSeconds = function() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('done');
    }, 2000)
  })
}

const runner = async function (){
  const res = await afterTwoSeconds(); 
  //afterTwoSeconds에서 'done'이라는 문자열을 resolve하고 있는데 unknown으로 됨
  console.log(res);
}

res는 resolve가 'done'이라는 문자열을 반환하지만 res는 unknown으로 유추된다.

const afterOneSecond = function(): Promise<string>{ // Promise를 리턴할 것이다.<resolve타입을 넣어주면 됨>
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('done');
    }, 1000)
  })
}

const runner2 = async function (){
  const res = await afterOneSecond(); 
  console.log(res); //string타입으로 반환받는다.
}

runner2();

해결법은
비동기로 반환을 할 거면은 Promise타입을 반환해주면 되고 어떤 값을 실제로 반환받을지는 Promise제너릭안에 넣으면 된다.

const runner3 = async function(){
  return 'string return'; 
  //이 함수는 비동기로직이 없는데도 async가 앞에 붙기만 하면 타입스크립트는 자동적으로 Promise를 유추한다.
}

이 함수는 비동기로직이 없는데도 async가 앞에 붙기만 하면 타입스크립트는 자동적으로 Promise를 유추한다.

profile
알고리즘 정리 블로그입니다.

0개의 댓글