본문 바로가기
카테고리 없음

TIR: Typescript | 함수(7) 다형성

by nomfang 2023. 1. 16.
728x90
반응형

다형성

지금까지의 타입들은 모두 구체타입이다

  • boolean
  • string
  • Date
  • () => void

기대하는 타입을 정확히 알고 있고
실제 타입이 전달되었는지 확인할 때는 구체 타입이 유용하다
하지만 어떤 타입을 사용할지 미리 알기 힘든 상황에는
함수를 특정 타입으로 제한하기 어렵다

배열 함수 filter 를 사용한다고 가정하면
filter 는 모든 타입에 대해 다룰 수 있어야 한다
오버로딩을 사용할 수 있지만 코드가 매우 지저분해진다

type Filter = {
     (array: number[], f: (item: number) => boolean): number[]
    (array: string[], f: (item: string) => boolean): string[]
    (array: object[], f: (item: object) => boolean): object[]
}

object 타입을 사용하는 것은 설계상 오류를 발생하게 한다
object 타입에는 실제 사용하려는 프로퍼티와 값이 정의되어있지 않기 때문

제네릭 타입 매개변수

여러 장소에 타입 수준의 제한을 적용할 때 사용하는 placeholder 타입
타형성 타입 매개변수라고도 부른다

제네릭 타입을 사용하면 다음과 같이 정의할 수 있다

type Filter = {
  <T>(array: T[], f: (item: T) => boolean): T[]
}

T 라는 제네릭 타입 매개변수를 사용하여 작성하였다
타입스크립트가 T라는 제네릭을 추론하도록 두는 것이다
T는 해당 타입에 대한 placeholder 로 타입의 자리를 맡아두는 역할을 한다
타입 검사기는 문맥을 보고 placeholder 타입을 실제 타입으로 채운다
이처럼 T는 filter의 타입을 매개변수화 한다
이 떄문에 T를 제네릭 타입 매개변수라고 부른다

제네릭 타입 매개변수는 제네릭 타입 혹은 제네릭이라고 부른다
<> 꺽쇠 괄호는 제네릭의 키워드로 보면 된다
제네릭 키워드를 추가할 수 있는 위치는 한정되어 있고
해당 위치에 따라 제네릭의 범위가 결정된다
해당 범위 내의 모든 매개변수 인스턴스는 한 개의 구체 타입으로 한정됨을 보장한다
필요에 따라 제네릭 키워드 <> 내에 여러 제네릭을 콤마로 구분하여 넣을 수 있다

어떤 인자가 전해지느냐에 따라 제네릭의 구체 타입이 추론, 결정된다

제네릭은 함수의 기능을 더 일반화하여 사용할 설명할 수 있는 강력한 도구
타입 별칭, 클래스, 인터페이스에서도 제네릭 타입을 사용할 수 있다

제네릭 타입은 언제 한정되는가

type Filter = {
  <T>(array: T[], f: (item: T) => boolean): T[]
}
let filter: Filter = (array, f) =>

이 예에서는 <T>를 호출 시그니처의 일부로 선언했으므로 (시그니처의 여는 괄호 바로 앞)
타입스크립트는 Filter 타입 별칭으로 한정하려면 Filter 를 사용할 때
타입을 명시적으로 한정한다

이와 달리 T의 범위를 Filter의 타입 별칭으로 한정하려면 Filter 사용 시
타입을 명시적으로 한정하도록 한다

type Filter<T> = {
 (array: T[], f: (item) => boolean): T[] 
}
let filter: Filter<number> = (array, f) => // 정상적인 사용

let filter: Filter = (array, f) => 
 // 에러: 제네릭 타입 'Filter' 는 한 개의 타입 인수를 요구함

보통 타입 스크립트는 제네릭 타입을 사용하는 순간에 제네릭과 구체 타입을 한정한다
함수에서는 함수 호출 시, 클래스에서는 클래스를 인스턴스화 할 때,
타입 별칭과 인터페이스에서는 사용하거나 구현할 때 타입을 한정한다

제네릭의 선언

타입스크립트에서는 호출 시그니처를 정의하는 방법에 따라 제네릭을 추가하는 방법이 정해진다

type Filter = {
  <T>(array: T[], f: (item: T) => boolean): T[]
}

let filter: Filter

type Filter<T> = {
  (array: T[], f: (item: T) => boolean): T[]
}
let filter: Filter<number>

type Filter<T> = {
  (array: T[], f: (item: T) => boolean): T[]
}

function filter<T>(array: T[], f: (item: T) => boolean): T[]


type Filter<T> = {
  (array: T[], f: (item: T) => boolean) => T[] // 단축 호출 시그니처
}

let filter: Filter<number>

type Filter = {
  <T>(array: T[], f: (item: T) => boolean) => T[] // 단축 호출 시그니처
}

let filter: Filter

자바스크립트의 표준 라이브러리의 많은 함수,
특히 Array 프로토타입이 제공하는 함수들은 제네릭을 사용한다
배열은 어떤 타입이든 값으로 포함할 수 있기 때문이다

제네릭의 타입 추론

대부분의 상황에서 타입스크립트는 제네릭을 훌륭하게 추론한다
그렇지만 제네릭을 명시적으로 지정할 수도 있다
제네릭을 명시할 때는 전부 하거나 아무것도 하지 않아야 한다

타입스크립트는 제네릭 함수로 전달한 인수 정보를 이용해
제네릭의 타입을 추론하므로 다음과 같은 상황이 벌어질 수도 있다

let promise = new Promise(resolve => resolve(45))
promise.then(result => result * 4) // {} 로 추론
    // 에러 TS2363: 수학 연산의 왼쪽 연산자는 'any', 
    //'number', 'bigint', enum 타입 중 하나여야 함
let promise = new Promise<number>(resolve => { 
    resolve(45) 
})
promise.then(result => result * 4) // number

위와 같이 제네릭을 명시하면 된다

제네릭 타입 별칭

click 등의 DOM 이벤트를 설명하는 Event 타입 정의

type Event<T> = {
    target: T
    type: string
}

타입 별칭에서는 타입 멸칭명과 할당 기호 사이에만 제네릭 타입을 선언할 수 있다

type ButtonEvent = Event<HTMLButtonElement>

taget 프로퍼티는 엘리먼트를 가르킨다
위와 같은 제네릭 타입을 사용하면 타입 추론이 되지 않으므로 명시적으로 매개변수를 한정해야 한다

타입 별칭을 준 Event를 다른 제네릭 타입을 정의하는 곳에 사용할 수 있다

type TimeEvent<T> = {
    event: Event<T>
    from: Date
      to: Date
}

TimeEvent의 T 를 한정할 때 Event에도 적용된다

제네릭 타입 별칭을 함수 시그니처에도 사용할 수 있다
타입스크립트는 T를 구체타입으로 한정하면서
제네릭 타입 별칭에도 적용한다

function triggerEvent<T>(event: Event<T>): void {

}

triggerEvent({
  target: document.querySelector('button')
  type: 'mouseover'
}) // T 는 Element | null

triggerEvent 호출 시
타입스크립트는 함수의 시그니처 정보를 이용해 전달된 인수가
Event<T> 여야 함을 인지한다
타입스크립트는 전달된 인수가 Element | null 임을 인지하고 모든 T 를 Element | null 타입으로 대체한다
그 후에 모든 타입이 할당성을 만족하는지 확인한다

한정된 다형성

이진트리는 최대 두 개의 자식 노드를 가질 수 있는 자료구조
U 가 T 타입을 포함할 때 U 가 T의 상한 한계라고 설명한다

type TreeNode = {
  value: string
}

type LeafNode = TreeNode & {
  ifLeaf: true
}

type InnerNode = TreeNode & {
  children: [TreeNode] | [TreeNode, TreeNode]
}

let a: TreeNote = { value: 'a' }
let b: LeafNote = { value: 'b', isLeaf: true }
let c: InnerNote = { value: 'c': children: [b] }

let a1 = mapNode(a, _ => _.toUpperCase()) // TreeNode

let b1 = mapNode(b, _ => _.toUpperCase()) // LeafNode
let c1 = mapNode(c, _ => _.toUpperCase()) // InnerNode

TreeNode 를 인수로 받아 value 에 매핑 함수를 적용해
새로운 TreeNode 를 반환하는 mapNode 함수가 있을 때
다음과 같이 타입 정의를 할 수 있다

function mapNode<T extends TreeNode>(
    node: T,
     f: (value: string) => string
): T {
      return {
        ...node,
        value: f(node.value)
      }
}

mapNode 는 한 개의 제네릭 타입 매개변수 T를 정의하는 함수다
T 의 상한 경계는 TreeNode 로, T 는 TreeNode 이거나 TreeNode 의 서브타입(하위 타입)이다

extends 를 생략하면 컴파일 타임에 에러가 발생한다
node.value 의 참조가 안전하지 않기 때문 (node 가 객체가 아닌 원시 타입일 수도 있고, node에 value 라는 프로퍼티 키가 없을 수도 있다)

T를 사용하지 않으면 LeafNode, InnerNode 에 대한 타입 매핑 정보가 보장되지 않아 모두 TreeNode 타입으로 반환 된다

T가 TreeNode 의 확장임을 extends 로 표현함으로
매핑 이후에도 노트가 각각 TreeNode, LeafNode, InnerNode 임을 보존할 수 있다

여러 제한을 적용한 한정된 다형성

여러 제한을 적용하기 위해서는 인터섹션(&) 으로
제한할 타입 들을 이어 붙이면 된다

type HasSides = { numberOfSides: number}
type SidesHaveLength = { sideLength: nubmer }
function logPerimeter<
     SHape extends HasSides & SidesHaveLength
 >(s: Shape): Shape {
    ...   
}

type Square = HasSides & SidesHaveLength

한정된 다형성으로 인수의 개수 정의

가변 인수 함수에서도 한정된 다형성을 사용할 수 있다
자바스크립트의 내장 call() 직접 구현 시 다음과 같이 할 수 있다 (call() 은 인수로 실행할 함수, 해당 함수에 들어갈 인수를 받아서 호출되는 함수)

function call(
    f: (...args: unknown[]) => unknown,
    args: unknown[].
    ): unknown {
    return f(...args)  
}

function fill(length: number, value: string): string[] {
    return Array.from({ length }, () => value) 
}

call(fill, 10, 'a') // 'a' 10개로 채워진 배열 반환

unknown 을 적절한 타입으로 변경 시 고려해야할 사항

  • f 는 T 타입의 인수를 n개 받아 R 타입을 반환하는 함수
  • call 은 f 한 개와 T n개를 받아 인수로 받은 T 들을 f가 다시 인수로 받는다
  • call 은 f의 반환 타입과 같은 R 타입을 반환한다

따라서 인수 배열 타입 T와 반환 값 R 두 타입의 매개변수가 필요하다

function call<T extends unknown[],R>(
    f: (...args: T) => R,
    ...args: T
): R {
      return f(...args)
}
)
  • call 은 가변 인수 함수로 T와 R 두 개의 타입 매개변수를 받는다 (T는 unkown[]의 서브타입으로 어떤 타입의 배열 또는 튜플이다)
  • call 의 첫 번째 인수는 함수 f (가변 인수 함수)로 args와 같은 타입의 인수를 받는다 따라서 args의 타입이 무엇이든 f 인수의 타입도 같다
  • f 함수 외에 call은 임의 개수의 매개변수 args 를 추가로 받는다
    args는 나머지 매개변수로 타입은 T이고 T는 배열타입 (T extends unkown[] 삭제 시 에러 발생) 이어야 한다
    args 로 전달한 인수를 보고 T에 맞는 튜플 타입을 타입스크립트가 추론한다
  • call() 은 f의 반환타입으로 한정된 R 타입의 값을 반환한다
let a = call(fill, 10, 'a') // string\[]
let b = call(fill, 'a', 'z') // error 

call 은 fill 을 사용하고 있기 때문에 fill 에 정의된
매개변수에 맞게 전달해야 한다

제네릭 타입 기본값

함수 매개변수에 기본 값을 설정할 수 있듯
제네릭 타입 매개변수에도 기본 타입을 정의할 수 있다

type Event<T extends HTMLElement = HTMLElement> = {
  target: T
  type: string
}

위와 같이 하면 특정 HTML 요소에 종속되지 않은 이벤트를 만들 수 있다

type myEvent: Event = {
  target: myElement,
  type: string
}

함수의 선택적 매개변수 처럼 기본 타입을 갖는 제네릭은
항상 뒤에 위치해야 한다

반응형

댓글