20210512 TypeSciprt 07 Generic : array, tuple, function (type alias, interface), class, extends, keyof, generic 관계성(union type 문제 해결법)

6 분 소요

TypeScript07

Generic

  • 어떤 함수에서 들어오는 매개변수에 대한 type과 return type에 대한 일정한 규칙 및 관계를 같게 하기 위해서 사용
  • 계속 똑같은 로직이지만 들어오는 type이 변하는 경우 type에 따른 return type을 나타낼때, type에 따른 함수를 개별적으로 반복적으로 만드는 것은 비효율적이다.
  • 그래서 type을 변수로 사용하여 들어오는 type에 따라 동적으로 사용하고 싶을 경우

문제의 시작

  • 같은 로직이지만 들어오는 type 만 다른 경우 -> 중복의 증가
function helloString(message: string): string {
  return message;
}

function helloNumber(message: number): number {
  return message;
}
  • any type 사용으로 인한 type 안정성 저하
    • any의 사용은 compile 상에서 문제를 못잡고 runtime에서 문제를 일으킴
function hello(message: any): any {
  return message;
}

// error
console.log(hello("Tom").length);
// any 형식의 length로 사용되어 짐
console.log(hello(2).length);
// number에는 length가 없는데 length가 사용되짐
// 이처럼 compile 상에는 문제가 없지만 runtime에서는 undefined 같은게 나오게 됨

Generic의 시작

  • Generic은 들어가는 type을 변수같은 것으로 활용해서 return 되는 type에 연관시켜주고 싶은 생각에서 시작됨
  • 함수 이름옆에 <T>라고 붙여 generic이라는 type 변수(type 할당은 X)를 내부적으로 사용함을 알려주고 내부적으로 type 변수를 사용할 수 있게함
  • 인자의 타입에 T, return에 type을 T 로 같게 만듦으로써 들어오는 인자의 type에 동적으로 return type도 연동해서 같게 할 수 있게 되는 것임
  • generic이 들어간 함수 호출시 특정한 type으로 제한한다고 표시안하면 type 추론시 리터럴 형태로 작게 추론됨
function helloGeneric<T>(message: T): T {
  return message;
}
console.log(helloGeneric("Tom").length);
// function helloGeneric<"Tom">(message: "Tom"): "Tom"
// 문자열 리터럴은 string의 서브 타입으로 들어가니까 length를 사용할 수 있음

console.log(helloGeneric(3));
// function helloGeneric<3>(message: 3): 3
// 숫자열 리터럴로 type이 지정됨 (length는 사용이 불가해지고 length 사용시 compile error 발생 )

console.log(helloGeneric(true));
// function helloGeneric<true>(message: true): true
  • Generic의 장점
    • any는 모든것을 받아서 모든것으로 주기 때문에 들어오는 type에 따라 type을 return하게 할수는 없음
    • generic은 type으로된 연산이 내부적으로 가능하게 됨으로써 들어오는 인자의 type을 동적으로 활용할 수 있다.




Generic type 지정 사용법 & 다중 Generic

Generic type 지정 사용

  • 위에서 처럼 type을 지정하지 않고 Generic 함수를 사용하여 값을 할당하면 값을 가지고 type을 추론하여 사용된다.
  • 그래서 값을 넣을 때, generic에 들어가는 type을 지정하여 추론하지 못하게 할수 있다.
  • 제너릭 함수 같은 경우 사용시에 <>를 사용해서 함수 사용시 안에 들어올 수 있는 type을 제한 가능하다.
function helloBasic<T>(message: T): T {
  return message;
}
helloBasic < string > 32; // error
helloBasic < string > "Tom"; // string으로 제한
helloBasic(36);
// 특별히 제너릭 변수를 표현 안하고 함수 호출하여 값만 넣으면
// 그 값으로 타입을 추론함 (TS는 추론시 가장 작은 범위의 type을 추론하게 되어 있음 -> 36 -> 36)


다중 Generic

  • generic 타입 변수는 T 부터 시작해서 여러개를 사용 할 수 도 있음
    • 다른 이름으로 가능하지만 type을 의미해서 일반적으로 T 부터 알파벳 순서로 사용됨
function helloMulti<T, U>(message: T, comment: U): T {
  return message; // return에서 여러 인자들을 합쳐서 가공하여 사용하는 경우
}

helloMulti < string, number > ("Jerry", 12); // 지정 형식
helloMulti(36, 39); // 추론 형식




Generic Array & Tuple

Array

  • 배열 형태의 인자를 받아 사용하는 경우 배열의 type에 동적으로 활용하기 위해서 generic을 사용하는 경우
  • 받아오는 배열에서 요소들의 type은 같은 것을 사용하는 것이 바람직 하지만, 다른 type의 요소를 섞어 쓰는 경우도 있다.
  • 이런 경우 type 추론시 다른타입까지 포괄하는 union 타입의 형태로 추론 됨
  • 그래서 class 상으로는 return 타입이 union으로 추론 되기 때문에 runtime을 거쳐 실제로 출력되는 요소가 다른 type 이여도 class에서 추론된 union type 과 충돌하게 되어 runtime시 의도치 않는 type이 나와 해당 type에 대한 함수사용에도 문제를 일으킴
function helloArray<T>(message: T[]): T {
  return message[0]; // T의 배열로 들어온 0번째 -> T 자체 타입이 return
}

helloArray(["Hello", "World"]);
// function helloArray<string>(message: string[]): string
// 두 개의 공통 type으로 추론 ->

helloArray(["hello", 5]);
// function helloArray<string | number>(message: (string | number)[]): string | number
// 두개가 다르므로 모두 가능한 type으로 추론(union으로 사용)
// 그러므로 return 타입은 무조건 string인데 type이 추론 연산 상으로는 유니온으로 두개가 발생하기 때문에 사용할수 있는 함수는 기대하는 것과 달리 number 함수까지 가능하게 됨 -> 문제

helloArray(["hello", 5]).toString;
// 문제! : 다른 타입의 함수가 가능하게 됨


Tuple

  • Tuple의 경우에는 배열안에 각 요소 마다 type이 지정 되어 있기 때문에 그런 문제가 생기지 않음
    • Tuple은 순서, 길이를 모두 고려하기 때문에
function helloTuple<T, K>(message: [T, K]): T {
  return message[0];
}

helloTuple(["Hello", "World"]); // function helloTuple<string, string>(message: [string, string]): string
helloTuple(["Hello", 5]); // function helloTuple<string, number>(message: [string, number]): string




Generic function (함수 만들때 type 지정하여 사용하는 경우)

  • 함수의 타입만 정의하고 그 정의에 generic을 사용하고 싶은 경우
  • function 키워드를 사용하여 함수 생성시 미리 만들어 놓은 함수 형식 type을 사용할 수 없음
  • type 키워드 또는 interface 키워드를 통해 만들어지 함수를 표현하는 type을 현재 만드려는 함수에 적용시키려면, Arrow 함수를 사용해야함

type alias 형식의 generic

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

const helloFunction1: HelloFunctionGeneric1 = <T>(message: T): T => {
  return message;
};


interace 형식의 generic

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

const helloFunction2: HelloFunctionGeneric2 = <T>(message: T): T => {
  return message;
};




Generic class

  • T 타입 변수는 class 전체에서 유효 범위 발생
  • class에서 generic 사용하려면 class 명 뒤에 <T>를 붙여 표시
class Person<T> {
	private _name: T;

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

new Person('Mark');
// constructor Person<string>(name: string): Person<string>
new Person<string>(39) // error : string 지정인데 number 안된다.

class Person1<T, K> {
	private _name: T;
	private _age: K;

	constructor(name: T, age: K) {
		this._name = name;
		this._age = age;
	}
}

new Person1('Tom', 23);
// constructor Person1<string, number>(name: string, age: number): Person1<string, number>
new Person1<string, number>("Mark", "age"); // error




generic with extends

  • T는 상속받으면 부모의 type안에서 밖에 못함
  • extends 키워드를 통해서 제너릭을 제한시킬 수 있음
  • generic을 사용하면 extends 키워드를 사용하는 것이 좋다! (일단 최대한 작게 type을 정해주는게 좋으므로)
class PersonExtends<T extends string | number> {
	private _name: T;
	constructor(name: T) {
		this._name = name;
	}
}

new PersonExtends('Tom');
new PersonExtends(23);
new PersonExtends(true); // Error: Argument of type 'boolean' is not assignable to parameter of type 'string | number'




keyof & type lookup system

  • keyof는 타겟 type앞에 붙여서 사용하면, 타겟 type의 구성하고 있는 key(property)를 union 형식으로 가짐
  • 단독으로 사용 못하고 어디에 할당해야 사용 가능
interface IPerson {
  name: string;
  age: number;
}

const person: IPerson = {
  name: "Tom",
  age: 39,
};

문제에 처리 과정

예제01

01) union 사용 문제

  • 매개변수와 return type에 대한 관계성을 가지기 때문에 일반적인 union 사용은 문제가 된다.
    • name - string, age - number 형식으로 되어야 하는데 엇갈릴 수 있음
  • 사용이 반복되는 여러 함수가 있을 경우, 함수 내에서 union을 일일이 고쳐 주어야 하는 문제 발생
function getProp(obj: IPerson, key: "name" | "age"): string | number {
  return obj[key];
}

02) union 중복 제거를 위한 keyof 사용

  • 일단, 굳이 type의 key를 일일이 union으로 써주지 않고,
  • type명에 keyof를 통해서 해당 key를 자동으로 가져오게 함(type이 선언 된곳에 수정하면 모든 곳에서 반영이 됨)
  • 하지만 관계성 문제는 여전히 해결하지 못함
  • type 자체는 결국 두가지를 모두 가지기 때문에 문제임 (선택되는 것이 아니라)
// 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];
}

03) Generic을 활용한 관계 만들어 주기

  • Generic을 사용해서 통일 시키는데,
  • 핵심은 key의 generic을 extends 를 통해서 union 타입의 형식으로 제한 시키고 다른 generic에 담아 선택하게 하면, 나중에 return type을 만들때 둘의 type을 관계시켜 적절한 type을 return 시킬 수 있음
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
	return obj[key];
}

getProp(person, 'name');
// 정확히 나옴 function getProp<IPerson, "name">(obj: IPerson, key: "name"): string




예제02

01) union 사용 문제

  • key 값과 value 값에 대한 type 마찰
function setProp(obj: IPerson, key: "name" | "age", value: string | number) {
  obj[key] = value;
}

02) union 중복 제거를 위한 keyof 사용

  • 중복제거는 되었지만, 관계가 아직 없음
function setProp(
	obj: IPerson,
	key:  keyof IPerson,
	value: string | number
): void {
	obj[key] = value;
}

03) Generic을 활용한 관계 만들어 주기

function setProp<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
	obj[key] = value;
}

setProp(person, 'name', 'kim');
// function setProp<IPerson, "name">(obj: IPerson, key: "name", value: string): void
setProp(person, 'age', 1);
// function setProp<IPerson, "age">(obj: IPerson, key: "age", value: number): void