본문 바로가기
블로그 이미지

방문해 주셔서 감사합니다! 항상 행복하세요!

  
   - 문의사항은 메일 또는 댓글로 언제든 연락주세요.
   - "해줘","답 내놔" 같은 질문은 답변드리지 않습니다.
   - 메일주소 : lts06069@naver.com


TypeScript

#2. 타입스크립트 제네릭, 타입(Typescript generics, type)

야근없는 행복한 삶을 위해 ~
by 마샤와 곰 2022. 5. 19.

타입스크립트(Typescript) 공부를 처음부터 다시 하면서 개념과 원리를 정리하고 있습니다.

 

타입스크립트(typescript)에서의 꽃은 제네릭과 타입 입니다.

제네릭은(generic) 자바(Java)라는 프로그래밍언어를 많이 접해본 사람 이라면 쉽게 다가갈 수 있는 영역 입니다.

데이터의 자료형을 지정해 주는 방법으로 지정된 이외의 다른 데이터가 들어가지 못하게 할 수 있습니다.

 

1. 제네릭(generic)

let array = []  //오직 숫자만 들어가야되는 배열이라 가정 합니다.
array.push(123)  //정상적인 입력1
array.push(456)  //정상적인 입력2
array.push('hello')  //어...이건 들어가면 안되는데...;

 

위 내용처럼 어떠한 숫자형식의 데이터만 필요로 하는 대상이 존재한다고 가정하면, 타입스크립트에서 해당 기능을 만들기 위해서는 간단하게 제네릭을 활용하면 됩니다.

let array = new Array<number>()  //꺽은 괄호에 숫자타입 number를 입력!
array.push(123)  //정상적인 입력1
array.push(456)  //정상적인 입력2
array.push('hello')  //오류발생!

 

위 코드에서 숫자 또는 문자만 받는 배열을 생성하고 싶으면 유니온을 제네릭안에 추가하여 주면 됩니다.

let array = new Array<number|string>()  //유니온을 활용한 타입을 2개로 선언
array.push(123)  //정상적인 입력1
array.push(456)  //정상적인 입력2
array.push('hello')  //오류가 발생하지 않습니다.

 

클래스나 함수에도 마찬가지로 제네릭을 적용 할 수 있습니다.

아래는 간단하게 클래스에 적용시킨 모습 입니다.

class MyClass <T> {
  tValue : T
  constructor(t : T){
     this.tValue = t;
  }

  print <T> (arg : T){
    console.log(arg, typeof arg)
  }

}

let myclass = new MyClass('string');  //문자를 넣어 문자형태로 인스턴스화 하였습니다.

 

T라는 타입은 대입되는 타입을 의미 합니다.

문자(string), 숫자(number) 및 사용자 정의 타입 등 어떠한 값이 들어와도 상관 없습니다.

클래스에서의 제네릭은 키(K)와 값(V)형식으로도 생성을 할 수 있습니다.

class MyClass2 <T, V> {

  tValue : T;
  vValue : V;

  constructor();  //오버로딩 생성자를 사용 해 보았습니다.
  constructor(t :T, v:V); 

  constructor(t? :T, v?: V){
    if(t) this.tValue = t;
    if(v) this.vValue = v;
  }

}

 

위 클래스는 제네릭으로 2개의 값을 받을 수 있습니다.

일반적으로 앞에 받는 값을 "키" 형태라고 의미하고 다음으로 오는 값은 "값" 형태라고 의미 합니다.

사용법은 제네릭을 1개만 받는 기존 방법과 동일합니다.

 

클래스 형태의 제네릭에서는 주의해야 되는 점은 상속을 받는 경우에는 타입을 반드시 선언해야 되는 것 입니다.

아래는 타입이 정해지지 않는 클래스를 상속받는 샘플코드입니다.

class MyClass2 <T, V> {

  tValue : T;
  vValue : V;

  constructor();  //오버로딩 생성자
  constructor(t :T, v:V);  //오버로딩 생성자

  constructor(t? :T, v?: V){
    if(t) this.tValue = t;
    if(v) this.vValue = v;
  }
}


class ExtendClass extends MyClass2<string, number>{  //상속을 받으면 반드시 타입을 지정해야 합니다.
  constructor(s : string, n : number){
    super()
    this.tValue = s;
    this.vValue = n;
  }
}

let cls = new ExtendClass('good', 1234);

 

제네릭 내부에서의 제한을 두는 방법이 1가지 더 존재 합니다.

타입에 대한 상속(extends) 입니다.

class OnlyStringClass{

  print<T extends string>(t : T){
    console.log(t)
  }
}

 

제네릭 내부에 타입(T)은 문자(string)를 상속받은 타입이거나 또는 문자만 받을 수 있게 하였습니다.

이러한 상속과 관련된 제네릭은 사용자가 정의한 클래스, 타입등을 주로 부여하여 데이터가 잘못 들어오는 것을 방지 하는 데 사용됩니다.

 

함수에서의 제네릭은 클래스형태와 비슷하게 사용 가능 합니다.

조금 다른점은 함수이름 옆에 제네릭 기호를 써 주어야 하는 것 입니다.

function print<T>(a : T, b: T){
  console.log(a, b)
}

print('hello', 'world')  //처음 파라미터가 문자이므로 다음 파라미터도 반드시 문자여야 합니다.

 

화살표 함수 또한 비슷한 방법을 사용하나 조금 다른 것은 제네릭 부호가 맨 앞에 온다는 점 입니다.

function print<T>(a : T, b: T){
  console.log(a, b)
}
print('hello', 'world')


//화살표 함수에서의 표기 방법!
let arrowFunction = <T>(a : T, b : T) : void=>{  //맨 앞에 제네릭을 선언 합니다.
  console.log(a, b)
}

arrowFunction('hello', 'world')

 

2. 타입(type)

타입(type)은 데이터의 형태를 정의하는 방법 입니다.

클래스와 인터페이스, 추상클래스는 new 연산자를 통해 객체로 생성하여 사용합니다.

그러나 타입(type)은 개발하는 코드에만 존재 할 뿐 실제 컴파일시 사라지게 되며 컴퓨터의 자원을 소비하지 않는 데 차이가 있습니다.

type hello = {
  name? : string,
  money? : number
}

type world = {
  korea : string,
  japan : number,
  china : any
}

//hello 타입의 객체
let helloObject : hello= {name : 'aa', money : 1234} 
helloObject.byebye = '잘가'  //컴파일 오류, 해당 타입에 byebye 속성이 없기 때문입니다.

//world 타입의 객체
let worldObject : world= {korea : 'aa', japan : 1234, china : ''}
worldObject.korea = 1234 //컴파일 오류, 해당 타입은 문자만 가능 합니다.

 

타입은 자바스크립트의 기본 자료형처럼 사용가능하며, 데이터의 형식을 정의하는 데 사용합니다.

숫자(number), 문자(string), 배열(Array) 등 처럼 타입을 사용하여 데이터의 제한을 둘 수 있습니다.

이렇게 정의된 타입은 유니온 연산자를 통해 다른 타입에 선택적으로 들어가게 할 수 있습니다.

type hello = {
  name? : string,
  money? : number
}

type world = {
  korea : string,
  japan : number,
  china : any
}

type helloWorld = hello | world  // hello타입이나 또는 world 타입을 허용 합니다.

 

helloWorld타입은 world 또는 hello 타입을 가질 수 있습니다.

타입으로 지정할 수 있는 갯수는 n개 입니다.

* name앞에 붙은 ? 기호는 옵셔널의 의미로 해당 값이 없어도 된다는 의미 입니다.

 

이러한 방법은 아래 코드처럼 유용하게 사용 됩니다.

type hello = {
  name? : string,
  money? : number
}

type world = {
  korea : string,
  japan : number,
  china : any
}

type helloWorld = hello | world //hello 또는 world 타입으로 사용가능 합니다.

class HelloWorldClass{
  print<T extends helloWorld>(t : T){  //helloWorld타입 또는 상속받은 타입만 가능 합니다.
    console.log(t)
  }
}

let test = new HelloWorldClass();
let helloObject : hello= {name : 'aa', money : 1234}
let worldObject : world= {korea : 'aa', japan : 1234, china : ''}

//서로 다른 타입이지만 오류가 나지 않습니다.
test.print(helloObject);  
test.print(worldObject);

 

타입에서의 keyof 연산자와 typeof 연산자가 존재 합니다.

keyof는 타입에서의 키 값들을 열거(enum)형태로 반환을 하고, typeof는 선언된 객체 또는 타입의 타입정보를 반환 합니다.

type world = {
  korea : string,
  japan : number,
  china : any
}

type copyWorldKey = keyof world  // 결과 : korea, japan, china
type copyWorldType = typeof world  // 결과 : world 타입

 

keyof는 정의된 타입에서의 키 값을 가져와 데이터의 형태를 바꾸는데 자주 사용됩니다.

위 world 타입에서의 korea, japan, china 타입을 전부 숫자로 바꾸어 보면 아래처럼 사용할 수 있습니다.

type world = {
  korea : string,
  japan : number,
  china : any
}

type copyWorldKey = keyof world  // 결과 : korea, japan, china
type copyWorldType = typeof world  // 결과 : world 타입


//K in keyof world 를 통해 키값들을 가져와 형태를 number로 바꿉니다.
//대괄호를 쓴 것은 자바스크립트 ECMA6문법인 계산된 속성이름(Computed Property Name) 방법 입니다.
//in 은 열거형 데이터에서의 값을 대입하는 방법 입니다. ex : for (arg in array) console.log(arg)
type reGenerateWorld = {[K in copyWorldKey] : number} 


//typeof를 통해 복사한 모습
let originWorld : copyWorldType ={
  korea : 'string',
  japan : 1234,
  china : new Array()
}

//keyof를 통해 데이터 형식을 바꾼 모습
let reWorld : reGenerateWorld ={
  korea : 1234,
  japan : 1234,
  china : 1234
}

 

타입에서의 기존에 생성된 데이터에 대해 매핑타입을 바꾸는 것도 존재 합니다.

아래 코드는 기존에 생성된 데이터의 값을 바꿀수 없는 형태로 변경하는 코드 입니다.

type ReadOnly<T> = { //타입을 받아서
  readonly [P in keyof T] : T[P]  //readonly제한을 두고 나머지 데이터 및 타입은 그대로 반환합니다.
}

let originData : any = {
  'str':'문자',
  'num' : 1234
}

function changeMappingType (arg : any){  //데이터를 받아서
  let ronly :ReadOnly<any> = arg;  //ReadOnly 타입으로 바꿉니다.
  return ronly
}

let noChange = changeMappingType(originData);  //해당 데이터는 이제 아무것도 바꿀 수 없습니다.
noChange.str = 'abcd'; //컴파일 오류
noChange.num = 1111 //컴파일 오류

 

 

ReadOnly타입은 키 값과 데이터는 그대로 보관하면서 readonly 제한자만 추가하는 타입입니다.

대괄호로 [P in keypf T]  라고 쓴 부분은 타입 T의 프로퍼티인 P를 그대로 넣으라는 명령어 입니다.

ECMA6 문법인 계산된 속성 이름(Computed Property Name)을 사용하는 기능 입니다.

 

위 경우에서 그러면 기존의 readonly를 제거하고 타입 값들을 선택 가능한 옵셔널(물음표 부호?) 형태로 바꾸려면 아래처럼 쉽게 가능 합니다.

type ReadOnly<T> = { //타입을 받아서
  readonly [P in keyof T] : T[P]  //readonly제한을 두고 나머지 데이터 및 타입은 그대로 반환합니다.
}

let originData : any = {
  'str':'문자',
  'num' : 1234
}

function changeMappingType (arg : any){  //데이터를 받아서
  let ronly :ReadOnly<any> = arg;  //ReadOnly 타입으로 바꿉니다.
  return ronly
}

let noChange = changeMappingType(originData);  //해당 데이터는 이제 아무것도 바꿀 수 없습니다.


//새로운 타입입니다.
type removeReadOnlyAndaddOptional<T> = {
   //빼기(-) 부호를 통해 속성을 빼고 물음표(?) 부호를 더해 옵셔널한 속성으로 변경 합니다.
  -readonly [P in keyof T]? : T[P]  
}
let changeObject : removeReadOnlyAndaddOptional<any> = noChange

changeObject.num = 4  //이제 바꿀 수 있습니다!!

 

자바(java)와 다르게 타입스크립트에서는 제네릭에 형태가 n개가 들어갈 수 있는 유니온 기능을 제공하고,

타입이라는 객체와 다른 형태의 데이터 표현방법이 존재하여 다소 처음에 이해하기 어려운 것은 사실입니다.

그래도 익숙해 지도록 열심히 연습해야 겠습니다!!

궁금한점 또는 틀린부분은 언제든 연락주세요!👻

 

반응형
* 위 에니메이션은 Html의 캔버스(canvas)기반으로 동작하는 기능 입니다. Html 캔버스 튜토리얼 도 한번 살펴보세요~ :)
* 직접 만든 Html 캔버스 애니메이션 도 한번 살펴보세요~ :)

댓글