제네릭 타입은 인터페이스나 함수 타입 별칭등에 사용할 수 있는 기능이다.
해당 심벌의 타입을 미리 지정하지 않고 다양한 타입에 대응하려고 하는 기능이다.
IValuable.ts
export interface IValuable<T>{
value: T
}
index.ts
import { IValuable } from "./IValuable";
export class Valuable<T> implements IValuable<T>{
constructor(public value: T) { }
}
export {IValuable}
printValue.ts
import { Valuable,IValuable } from "./index";
export const printValue = <T>(o: IValuable<T>): void => console.log(o.value)
export {Valuable,IValuable}
value-test.ts
import { printValue,Valuable } from "./printValue";
printValue(new Valuable<number>(1))
printValue(new Valuable<boolean>(true))
printValue(new Valuable<string>('hello'))
printValue(new Valuable<number[]>([2,3,4]))
실행결과
1
true
hello
[ 2, 3, 4 ]
위와 같은 형식으로 제네릭을 사용할 수 있다.
코드가 길어 보여서 뭔가 복잡해보이는데 별거아니다
그리고 타입스크립트는 저런식으로 제네릭안에 타입을 넣지않더라도
타입추론을 통해서 알아서 작동하긴 한다.
import { printValue,Valuable } from "./printValue";
printValue(new Valuable(1))
printValue(new Valuable(true))
printValue(new Valuable('hello'))
printValue(new Valuable([2,3,4]))
실행결과
1
true
hello
[ 2, 3, 4 ]
제네릭 예약은 타입 변수에 적용할 수 있는 타입의 범위를 한정하는 기능을 한다.
타입스크립트에서 제네릭 함수의 타입을 제한하고 싶을때는 다음과 같은 구문을 사용한다.
<최종타입1 extends 타입1, 최종타입2 extends 타입2>(a: 최종타입1,b: 최종타입 2,...){}
printValueT.ts
import { IValuable } from "./IValuable";
export const printValueT = <Q,T extends IValuable<Q>>(o: T) => console.log(o.value)
export {IValuable}
printValueT-test.ts
import { printValueT,IValuable } from "./printValueT";
import { Valuable } from "./printValue";
printValueT(new Valuable(1))
printValueT({value: true}
실행결과
1
true
매개변수 타입을 어느 방식으로 제약하느냐만 다르고 사용방법을 동일하다.
const create = <T>(type: T): T => new type()
위코드는 오류가 발생한다.
타입스크립트에서 타입의 타입을 허용하지 않기 때문이다.
그래서 타입의 타입을 발생시키는것 보다는 다음과 같이 사용할 수 있다.
const create = <T extends {new(): T}>(type: T) => new type()
혹은 중괄호를 생략해서 더 간결하게 표현 할 수도 있다.
const create = <T>(type: new() => T):T => new type()
결론적으로 {new(): T}와 new() => T 는 같다는 의미가 된다.
new 연산자를 type에 적용하면서 type의 생성자 쪽으로 매개변수를 전달해야 할 때 다음처럼 new(...args) 구문을 사용한다.
const create = <T>(type: {new(...args):T},...args): T => new type(...args)
create.ts
export const create = <T>(type: {new(...args): T}, ...args):T => new type(...args)
create-test.ts
import { create } from "./create";
class Point {constructor(public x: number, public y: number) { }}
[
create(Date),
create(Point,0,0)
].forEach(s=>console.log(s))
실행결과
2022-07-29T07:59:39.557Z
Point { x: 0, y: 0 }
이런식으로 잘 작동하는 모습을 볼 수 있다.
객체의 일정속성만 추려서 더 단순한 객체를 만들려고 할때
다음과 같이 일부속성만 추출해서 간단한 형태로 만들 수 있다.
const obj = {name: 'Jane', age: 22, city: 'Seoul', country: 'Korea'}
pick(obj,['name','obj'])
위 pick 함수는 다음과 같이 구현할 수 있다.
pick.ts
export const pick = (obj,keys) => keys.map(key => ({[key]: obj[key]}))
.reduce((result,value) => ({...result,...value}), {})
pick-test.ts
import { pick } from "./pick";
const obj = {name: 'Jane', age: 22, city: 'Seoul', country: 'Korea'}
console.log(
pick(obj,['name','age']),
pick(obj,['nam','agge'])
)
실행결과
{ name: 'Jane', age: 22 } { nam: undefined, agge: undefined }
이런식으로 key를 잘못입력하는 경우 원하지 않는 값이 나오게 된다.
이런일을 방지하기 위해서 인덱스 타입제약을 사용할 수 있다.
위에서 구현한 pick 함수와 다르게 T와 K라는 타입변수를 적용해서 코드를 짜면
다음과 같은 오류가 발생하게 된다.
이 오류메시지를 해결하기 위해서는 K가 T의 key라는것을 알려줘야 한다.
이때 타입스크립트의 인덱스 타입 제약을 사용한다.
오류가 사라진 모습을 볼 수 있다.
그러면 이제 위와 같이 못된 입력을 미리 방지할 수 있게 된다.