it공부 (개념)/javascript

Generic(제네릭)이란? 개념 및 Typescript 예제. "any 쓰지마세요"

cantor 2023. 1. 26. 21:00

 

 

Generic?

    • 사전적 의미: 일반적인, 추상적인, 포괄적인
      반의어) 구체적 (specific)



  • Generic in TypeScirpt(타입스크립트, 이하 TS)

    TS의 "유연한 타입 제약"을 가능케하는 문법.

    하나의 함수, 또는 클래스 코드가 여러 타입의 객체를
    받을 수 있게. 해준다. 그래서 코드의 간결함과 가독성, 재사용성등을 향상 시킬수 있다.

    TS는 타입에 제약조건을 걸어 오류가없고 깔끔한 JS코드를 생성하기위해 만들어진 언어이다.
    하지만 여러타입의 객체에대해 작업을 수행하는 코드를 작성할때
    타입별로 비슷한내용의 코드를 반복해서 작성해야 하는 번거로움이 있다.

    Generic을 사용하면 이런 수고로움을 덜 수있다.



    Generic은 JS에는 존재하지 않는 TS만의 구성요소이다. 

 

Genric 문법 TS예제

 

//제네릭 문법 <parameter Symbol(매개변수 기호)>
//선언 방법<>내부에 임의의 기호심볼을 집어넣는다.
//보통 매개변수 기호로 T나 U를 많이 사용한다.
//ex) <T>, <T, U>


function printValue<T>(input: T) {
    console.log(input);
}


//함수를 호출하는 예제
//<>내부에 원하는 type을 입력해준다.

printValue<string>("hello");
printValue<number>(7)




// JS로 컴파일(트랜스파일) 된 코드
// Generic은 TS의 구성요소이기때문에, 관련코드는 지워진다.

function printValue(input) {
    console.log(input);
}
printValue("hello");
printValue(7);

 

출력 결과

 

Generic 타입에 제약조건 걸기

 

선언하는 함수의 parameter에 제약조건을 걸고 싶다면

"extends" 키워드를 사용한다.

function doubleArray<T extends any[]> (input: T){
    return [...input,...input];
    }

출력결과

 

 

기타예시

let x: string;
x = 'hello';
let y: number;
y = 7;

//변수를 넣어 호출하기
printValue<string>(x);
printValue<number>(y);


//arrow function를 통하여 
//Generic function을 여러버전으로 변수에 저장하기

//string 버전


const printString = (input: string) => printValue<string>(input);

printString("hi");

//number 버전
const printNumber = (input: number) => printValue<number>(input);

printNumber(99);

 

Generic을 사용할 수 있는 위치

 

 

제네릭<T>은  다음과 같은 곳에 사용할수있다.

 

  • 클래스의 인스턴스 성분
  • 클래스 메서드의 입력값
  • 함수의 매개변수 타입
  • 함수의 return값 타입

 

 

예제코드

// 클래스의 인스턴스성분, 클래스 메서드의 입력값

class genericClass<T> {
    genericProperty: T;
    constructor(input: T) {
        this.genericProperty = input;
    }
    genericMethod(input: T): T {
        return input;
    }
}


// 함수의 parameter 타입( 혹은 argument 타입) 그리고 return 타입
function genericReturn<T>(input: T): T {
    return input;
}

 

Generic을 왜 사용할까?

 

 

JS는 Dynamic typing(동적 타이핑) 언어이다.

때문에 타입 제약에서 자유로운 코딩이 가능하다.

 

그렇기 때문에 프로그래밍 코드에서 변수를 선언할 때
그 변수의 타입(number, string, boolean 등)을 표기하지 않는다.

// JS코드

let name = 'this is string';
let value = 7;

 

하지만 그 때문에 코드가 작성의도와 다르게 작동해도 문제의 원인을 진단하기 어렵다.

대규모 프로젝트의 유지보수도 굉장히 어렵다.

 

 

그래서 JS의 타입에 "제약"을 걸어 JAVA처럼 Static typing(정적 타이핑) 언어처럼

프로그래밍하고 싶은 의도에서 TS가 탄생했다.

// TS코드

let name : string;
name = "this is string";
let value : number;
value = 7;


// 물론 변수선언과 초기화를 동시에하는 경우,
// TS에서도 타입을 표기하지 않을때도 있다.

 



Generic의 사용이유도 이와 일맥 상통한다. 바로 제약이다.

유연성을 확보하면서도 JS의 오류-친화적 개발을 지양하기위해 사용한다.

 

 

TS는 Generic을 사용하지 않고도 여러 타입의 요소에도 반응하는 코드를 작성할 수 있다.

 

"any" 변수지정 키워드나 Union type,  function overloading 등의 방법이 그것이다.

하지만 저것들보다 generic을 선호하는 이유는 다음과 같다.

 

 

    • 1. any를 사용하는 TS는 JS와 차이가 없다.

      제약이 주는 이점: 오류의 최소화를 잃는다.

 

    • 2. Union type 혹은 functional overloading을사용하면 코드가 복잡해진다.

      타입을 일일이 나열하거나, 타입별 코드를 개별적으로 작성해주어야 한다. 
      결과적으로 코드가 길어지고, 가독성과 재사용성이 떨어진다.



  • 3. +@ TS 컴파일러의 "타입추론" 을 더 잘할 수 있게 해준다.

 

 

 

1. Any를 사용하는 TS는 JS와 차이가 없다.

먼저 any는 어떤 경우에 사용할 수 있으며, 왜 사용하지 않는지 살펴보자.

 

 

다음의 코드는 입력값을 그대로 반환하는 JS코드이다.

// 타입을 신경쓰지않는 자바스크립트코드

function returnValue(input) {
    return input;
}

console.log(returnValue(3));
console.log(returnValue('3'));

JS는 타입에 제약을 받지 않는 동적언어이기 때문에 

input의 타입을 지정해 주는 일반적인 방법이 없다.

 

우리는 위의 코드를 아무 문제없이 컴파일 할 수 있다.

 

출력결과

 

 

하지만 위의 코드를 TS로 작성하면 컴파일러는 다음과 같은 오류메시지를 띄워준다.

parameter는 input을 "any"타입으로 지정해줘야 한다.

 

타입스크립트 컴파일러의 판단은 이렇다.

console.log(returnValue(3));
// returnValue 함수가 number 타입 3을 입력받는다.
console.log(returnValue('3'));
// returnValue 함수가 string 타입 '3'을 입력받는다.

//returnValue 함수의 input은 number, string을 입력받을 수 있어야한다.
//그러니 parameter의 타입을 any로 명시해주면 되겠다.

//ts 코드

function returnValue (input: any) {
    return input
}

console.log(returnValue(3));
console.log(returnValue('3'));

 

위코드는  input이 input: any로 바뀐 것 말고는

일반적인 JS코드와 아무런 차이가 없다.

 

타입제약을 달아서 JS의 "아무 타입이나 일단 받기"를 제한하는 TS의 이점을 전혀 누릴 수 없다.

그래서 any의 사용을 지양해야 한다.

 

 

 

 

ts 공식 doc문서에서도 any를 사용하지 말라고 권고한다.

https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html#any

요약:

Any를 사용하지 마시오

JS로 짜인 코드를 TS코드로 번경하는 과정 중에서 급하게 땜빵을 할 때에만 사용하시오.

 

 

구글에 검색해 봐도 Any 사용을 금하는 글들로 가득하다.

구글 검색 "any" 사용하지 마세요.

 

이다음에 나오는 이점은 any를 사용하지 않기로 결정했기 때문에 오는 이점이다.

 

 

2. Union type, functional overloading 보다 간결한 코드를 짤 수 있다.

 

any를 사용하지 않는다면 우리는 typescript에게 무엇을 시킬 수 있을까? 

다음은 자기 자신을 반환하는 함수이다.

//JS코드
function returnSelf (input) {
    return input;
}

 

위의 코드를 any 키워드를 사용하지 않고 타입 스크립트로 구현하려면 어떻게 해야 할까?

 

두 가지 방법이 있다.

1. function overloading

2. Union type

 

*function overloading 하나의 이름으로 입력 변수의 종류에 따라 다른 기능을 수행하는 함수를 정의하는 기능이다.

위의 경우엔 똑같은 기능을 수행하지만 원하는 타입에대하여 input: 타입 을 받는 함수를 전부 작성해주어야한다.

*Union type은 사용이 가능한 타입을 병렬로 늘어놓는 코드이다.

 

union type 포스팅보기

https://batcave.tistory.com/38

 

union type(유니온 타입)과 type guard(타입가드)란? 개념 및 Typescript예제

Union? 사전적 의미: 조합, 결합, 연합 in Mathematics(수학에서): 합집합 in Typescript(타입스크립트에서, 이하 TS): union type은 한 요소의 잠재적 타입들을 열거해 놓은 타입이다. "|" 기호를 사용하여 나타

batcave.tistory.com

 

 

 

//JS코드
function returnSelf (input) {
    return input;
}

이제 이 JS코드를 TS로 구현해보자

//function overloading 버전: any를 사용하지 않았다.

function returnSelf1(input: string): string;
function returnSelf1(input: number): number;
function returnSelf1(input: boolean): boolean;
function returnSelf1(input: object): object;
function returnSelf1(input: unknown): unknown {
    return input;
}


//Union type 버전: 역시 any를 사용하지 않았다.

function returnSelf2(input: string | number | boolean | object): string | number | boolean | object {
    return input;
}


// 위의 코드들은 함수에 입력받고 싶은 변수의 타입을 작성해야 한다.
//그래서 input: any를 사용할때보다 코드의 가독성이 훨씬떨어진다.


//generic 버전: 간결하고, 코드의 작성 및 재사용이 용이하다.

returnSelf3<T>(input: T): T {
    return input;
    }

위의 TS함수들은 세 개의 동일한 JS함수들로 변환된다.

//위의 TS코드의 컴파일결과 
//JS코드

function returnSelf1(input) {
    return input;
}
function returnSelf2(input) {
    return input;
}
function returnSelf3(input) {
    return input;
}

 

 

 

3. +@ type inference (타입추론)에 더적합하다.

 

TS는 "type inference"(타입추론)이라는 기능을 갖고있다.

변수의 타입을 정확히 명시하지 않아도 TS가 알아서 변수를 판단하는 기능이다.

 

만약에 TS가 타입추론을 실패할경우, 에러메시지를 띄워준다.

 

타입추론예제

//변수의 선언과 초기화를 동시에 하는경우, 
// 타입을 표시해주지 않아도 TS는 알아서 타입을 이해한다.
// := 오류가 발생하지 않는다

let languageName = "TypeScript"; 
let numTen = 10; 
let isBoolean = true; 
let names = ["John", "Jane", "Bob"];

 

 

 

이제 TS가 타입추론에 실패해서 오류가 발생하는 경우를 살펴보자

 

//두개의 object를 합치는 함수이다.

function merge2(objA: object, objB: object) {
    return Object.assign(objA, objB);
}
const mergeObj2 = merge2({name: 'contor', hobbies: ['programming']}, {age: 77})
console.log(mergeObj2.name);

// mergeObj2.name 코드에서 오류가발생한다

 

위의 코드에서 함수를 호출할때 오류가 발생하는 이유는 object가 굉장히 넓은 범위를 포괄하는 용어이기 때문이다.

JS는 function, array 등이 전부  object로 구성되어있다.

 

 그래서 "objA: object"는 TS컴파일러에게 명확한 타입정보를 제공하지 못한다.

결과적으로 TS는 타입추론을 실패하고, 오류메시지를 출력한다.

 

 

 

 

다음은 generic으로 작성한 오류없는 코드이다.

function merge<T extends object, U extends object>(objA: T, objB: U) {
    return Object.assign(objA, objB);
}

const mergeObj = merge({name: 'contor', hobbies: ['programming']}, {age: 77});
console.log(mergeObj.name);

//위의 코드에선 오류가 발생하지 않는다

//merge2코드와 다른점은 generic타입이 선언되었다는 것 뿐이다.

 

 T extends object는 object보다  훨씬 좁은 범위의 타입을 나타낸다.

 

{} 안에 요소를 나열한 타입을 나타내는것이다.

 

그래서 Generic을 사용한 코드에서는 TS 컴파일러가 정확한 타입추론을 할 수 있게 된다.

 

 

이미지출처:

Data collection icons created by Becris - Flaticon

 

참고문서:

https://basarat.gitbook.io/typescript/type-system/generics

 

읽어주셔서 감사합니다.

잘못된 내용이 있으면 정정해주세요.