Million Dreams
100만개의 꿈을 꾸는 개발자 지망생
Do it! 타입스크립트 프로그래밍 10장

Ch10 제네릭 프로그래밍

10-1) 제네릭 타입 이해하기

- 제네릭 타입은 인터페이스나 클래스, 함수, 타입 별칭 등에 사용할 수 있는 기능으로, 해당 심벌의 타입을 미리 지정하지 않고 다양한 타입에 대응하려고 할 때 사용한다.

 

//제네릭 인터페이스 구문

Interface IValuable<T> {

  Value: T

}

- 어떤 인터페이스가 value라는 이름의 속성을 가질 때, 속성의 타입을 다음처럼 string, number 등으로 특정하지 않고 T로 지정해 제네릭 타입으로 만들 수 있으며, 인터페이스 이름 뒤에 <T>로 표기한다.

 

(1) 제네릭 사용하기

- 제네릭 인터페이스 정의

export interface IValuable<T> {

    value : T

}

 

- 제네릭 인터페이스 IValuable<T>를 구현하는 제네릭 클래스는 자신이 가진 타입 변수 T를 인터페이스 쪽 제네릭 타입 변수로 넘길 수 있다.

 

import {IValuablefrom './IValuable'

 

export class Valuable<Timplements IValuable<T> {

    constructor(public value: T) {}

}

 

export {IValuable}

 

- IValuable<T>, Valuable<T>를 사용하는 제네릭 함수는 다음처럼 자신의 타입 변수 T를 제네릭 인터페이스의 타입 변수 쪽으로 넘기는 형태로 구현할 수 있다.

import {IValuableValuablefrom './Valuable'

 

export const printValue = <T>(o: IValuable<T>): void => console.log(o.value)

export {IValuableValuable}

 

제네릭함수 사용 예시

import {printValueValuablefrom './printValue'

 

printValue(new Valuable<number>(1))

printValue(new Valuable<boolean>(true))

printValue(new Valuable<string>('hello'))

printValue(new Valuable<number[]>([123]))

 

10-2) 제네릭 타입 제약

- 프로그래밍 언어에서 제네릭 타입 제약(generic type constraint)은 타입 변수에 적용할 수 있는 타입의 범위를 한정하는 기능을 한다.

<최종타입1 extend 타입1, 최종 타입2 extend 타입2>(a: 최종 타입1, b: 최종 타입2, …) {}

 

타입 제약 구문 예시

import {IValuablefrom './IValuable'

 

export const printValueT = <QT extends IValuable<Q>>(o: T) => console.log(o.value)

export {IValuable}

 

- 이때 주의해야할 점은

printValueT <T extnds IValuable<T>>(o: T) 이런식으로 구현하면 안된다.

그 이유는 매개변수 oT입장에서 타입 TIValuable<T>이므로, 타입스크립트는 IValuable <IValuable<T>>로 해석하기 때문이다.

 

(1) new 타입 제약

-  프로그래밍 분야에서 팩토리 함수(factory function)new 연산자를 사용해 객체를 생성하는 기능을 하는 함수를 의미하며, 객체 생성 방법을 단순화하려는 목적을 가진다.

 

- 타입스크립트에서는 타입의 타입을 허용하지 않으므로, 타입스크립트 언어의 창시자이자 C# 언어의 창시자인 아네르스 하일스베르(Anders Hejlsberg)C# 언어에서의 구문을 빌려 타입스크립트 구문으로 만들었다.

 

Const create = <T extends {new(): T}>(type: T): 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)

 

new 타입 제약을 생성하는 예

export const create = <T>(type: {new(...args): T}, ...args): T => new type(...args)

 

(2) 인덱스 타입 제약

- 객체의 일정 속성들만 추려서 좀 더 단순한 객체를 만들어야 할 때는 인덱스 타입 제약을 사용한다.

 

예시

export const pick = (objkeys) => keys.map(key => ({[key]: obj[key]}))

        .reduce((resultvalue) => ({...result...value}), {})

 

import {pickfrom './pick'

 

const obj = {name: 'Jane', age: 22, city: 'Seoul', country: 'Korea'}

console.log(

    pick(obj, ['name''age']), 

    pick(obj, ['nam''agge'])

)

 

- 위 예시처럼 오타가 발생하면 엉뚱한 결과가 나오므로 이를 방지할 목적으로 타입스크립트에서는 keyof T 형태로 타입제약을 설정할 수 있게 지원하며 이를 인덱스 타입 제약(index type constraint)라고 한다.

<T, K extends keyof T>

 

export const pick = <TK extends keyof T>(obj: Tkeys: K[]) =>

 keys.map(key => ({[key]: obj[key]}))

        .reduce((resultvalue) => ({...result...value}), {})

 

10-3) 대수 데이터 타입

- 객체 지향 프로그래밍 언어에서 ADT라는 용어는 추상 데이터 타입(abstract data type)’을 의미하지만, 함수형 언어에서는 대수 데이터 타입(algebraic data type)을 의미한다.

 

- 타입스크립트에서 대수 데이터 타입은 합집합 타입(union type)’교집합 타입(intersection type)’ 두 가지 종류가 있다.

 

(1) 합집합 타입

- ‘또는(or)’의 의미인 ‘|’ 기호로 다양한 타입을 연결해서 만든 타입을 말한다.

type NumberOrString = number | string

let ns: NumberOrString = 1

ns = 'hello'

 

 

(2) 교집합 타입

- ‘이고(and)’의 의미인 ‘&’ 기호로 다양한 타입을 연결해서 만든 타입을 말한다.

- 대표적인 교집합 타입의 예는 두 개의 객체를 통합해서 새로운 객체를 만드는 것이다.

 

두 개의 객체를 결합한 예

export const mergeObjects = <TU>(a: Tb: U): T & U => ({...a...b})

 

import {mergeObjectsfrom './mergeObjects'

 

type INameable = {name: string}

type IAgeable = {age: number}

 

const nameAndAge: INameable & IAgeable = mergeObjects({name: 'Jack'}, {age: 32})

console.log(nameAndAge)

 

 

(3) 합집합 타입 구분하기

Ex) 세 개의 인터페이스와 각각의 인터페이스로 만든 객체를 가정

interface ISquare {size: number}

interface IRectangle {width: numberheight: number}

interface ICircle {radius: number}

 

const square: ISquare = {size: 10}

const rectangle: IRectangle = {width: 4, height: 5}

const circle: ICircle = {radius: 10}

 

위 객체들을 받아 면적(area)을 계산해 주는 calcArea라는 함수를 가정

type IShape = ISquare | IRectangle | ICircle

export const calcArea = (shape: IShape): number => {

    return 0

 

하지만 이 경우 shape 객체가 구체적으로 어떤 타입인지 알 수 없으므로 이를 해결할 수 있도록 합집합의 타입을 각각 구분할 수 있게하는 식별 합집합(discriminated unions)’이라는 구문을 제공한다.

 

(4) 식별 합집합 구문

- 식별 합집합 구문을 사용하려면 합집합 타입을 구성하는 인터페이스들이 모두 똑 같은 이름의 속성을 가지고 있어야 한다. (공통 속성 : tag를 구현)

export interface ISquare {tag: 'square'size: number}

export interface IRectangle {tag: 'rectangle'width: numberheight: number}

export interface ICircle {tag: 'circle'radius: number}

 

export type IShape = ISquare | IRectangle | ICircle

 

- switch문을 사용해 tag 값이 무엇이냐에 따라 적용될 함수를 구분

import {IShapefrom './IShape'

 

export const calcArea = (shape: IShape): number => {

    switch(shape.tag) {

        case 'square' : return shape.size * shape.size

        case 'rectangle' : return shape.width * shape.height

        case 'circle' : return Math.PI * shape.radius * shape.radius

    }

 

}

 

10-4) 타입 가드

- 두 개의 타입을 가정하고 아래와 같이 매개변수 타입을 합집합 타입으로 할 때, 객체가 무엇인지 구체적으로 알기 어렵다는 문제가 발생한다.

export class Bird {fly() {console.log(`I'm flying.`)}}

export class Fish {swim() {console.log(`I'm swimming`)}}

 

(1) instanceof 연산자

- 자바스크립트는 instanceof라는 연산자를 제공하며, 이 연산자는 두개의 피연산자가 필요하다.

객체 instanceof 타입 // boolean 타입의 값 반환

 

instanceof를 사용해 구현한 예

import {BirdFishfrom './BirdAndFish'

 

export const flyOrSwim = (o: Bird | Fish): void => {

    if(o instanceof Bird) {

        (o as Bird).fly() // 혹은 (<Bird>o).fly()

    } else if(o instanceof Fish) {

        (<Fish>o).swim() // 혹은 (o as Fish).swim()

    }

}

 

(2) 타입 가드

- 타입스크립트에서 instanceof 연산자는 자바스크립트와는 다르게 타입 가드(type guard)’ 기능이 있는데, 타입 가드는 타입을 변환하지 않은 코드 때문에 프로그램이 비정상으로 종료되는 상황을 보호해 준다는 의미이다.

 

export const flyOrSwim = (o: Bird | Fish): void => {

    if(o instanceof Bird) {

        o.fly() 

    } else if(o instanceof Fish) {

        o.swim() 

    }

}

 

 

(3) is 연산자를 활용한 사용자 정의 타입 가드 함수 제작

- 개발자 코드에서 instanceof처럼 타입 가드 기능을 하는 함수를 구현할 수 있는데, 이 때 함수의 반환 타입 부분에 is라는 이름의 연산자를 사용해야 한다.

 

사용자 정의 타입 가드 함수 예

import {BirdFishfrom './BirdAndFish'

 

export const isFlyable = (o: Bird | Fish): o is Bird => {

    return o instanceof Bird

}

 

 

import {BirdFishfrom './BirdAndFish'

 

export const isSwimmable = (o: Bird | Fish): o is Fish => {

    return o instanceof Fish

}

 

 

- 단 사용자 정의 타입 가드 함수는 if문에서 사용해야한다.

사용자 정의 타입 가드함수 사용 예

import {BirdFishfrom './BirdAndFish'

import {isFlyablefrom './isFlyable'

import {isSwimmablefrom './isSwimmable'

 

export const swimOrFly = (o: Fish | Bird) => {

    if(isSwimmable(o))

        o.swim()

    else if(isFlyable(o))

        o.fly()

}

 

import {BirdFishfrom './BirdAndFish'

import {swimOrFlyfrom './swimOrFly'

 

[new Birdnew Fish].forEach(swimOrFly)

 

10-5) F-바운드 다형성

(1)this 타입과 F-바운드 다형성

- 타입스크립트에서 this 키워드는 타입으로도 사용됨. This가 타입으로 사용되면 객체지향 언어에서 의미하는 다형성(polymorphism) 효과가 나는데, 일반적으로 다형성과 구분하기 위해 this 타입으로 인한 다형성을 ‘F-바운드 다형성(F-bound polymorphism)이라고 한다.

F-바운드 타입

- 자신을 구현하거나, 상속하는 서브타입(subtype)을 포함하는 타입을 말한다.

export interface IValueProvider<T> {

    value(): T

}

- IvalueProvider 인터페이스는 자신을 상속하는 타입이 포함되어있지 않은 일반 타입

 

export interface IAddable<T> {

    add(value: T): this

}

- Iaddable<T> add 메서드가 내가 아닌 나를 상속하는 타입을 반환하는 F-바운드 타입

 

export interface IMultiplyable<T> {

    multiply(value: T): this

}

- Imultiplyable 역시 반환 타입이 this 이므로 F-바운드 타입

 

IvalueProvider<T> 인터페이스의 구현

- IvalueProvider<T> 인터페이스를 구현하는 Class Calculator를 만들어, _value 속성을 private로 만든 후, _value속성이 아닌 value() 메서드로 접근할 수 있게 설계함.

 

import {IValueProviderfrom '../interfaces'

 

export class Calculator implements IValueProvider<number> {

    constructor(private _value: number = 0) {}

    value(): number {return this._value}

}

 

- 같은 방식으로 StringComposer란 클래스로 IvalueProvider<T>를 구현

 

import {IValueProviderfrom '../interfaces'

 

export class StringComposer implements IValueProvider<string> {

    constructor(private _value: string = '') {}

    value(): string {

        return this._value

    }

}

 

Iaddable<T>Imultiplyable<T> 인터페이스 구현

- Calculatoradd 메서드는 클래스의 this 값을 반환하며, 이는 메서드 체인(method chain)을 구현하기 위함이며, multiply도 마찬가지임

import {IValueProviderIAddableIMultiplyablefrom '../interfaces'

 

export class Calculator implements IValueProvider<number>, IAddable<number>, IMultiplyable<number> {

    constructor(private _value: number = 0) {}

    value(): number {return this._value}

    add(value: number): this {

        this._value = this._value + value

        return this

    }

    multiply(value: number): this {

        this._value = this._value * valuereturn this

    }

}

 

※ 테스트

import {Calculatorfrom '../classes/Calculator'

 

const value = (new Calculator(1))

            .add(2)

            .add(3)

            .multiply(4)

            .value()

console.log(value)

 

- StringComposer도 다음과 같이 구현할 수 있으며, Iaddable<T>, Imultiplyable<T>의 각각의 메소드들은 클래스를 어떻게 구현하느냐에 따라 반환 타입이 Calculator 방식 혹은 StringComposer 방식이 되기도 한다.

import {IValueProviderIAddableIMultiplyablefrom '../interfaces'

 

export class StringComposer implements IValueProvider<string>,  IValueProvider<string>, IAddable<string>{

    constructor(private _value: string = '') {}

    value(): string {

        return this._value

    }

    add(value: string): this {this._value = this._value.concat(value); return this}

    multiply(repeat: number): this {

        const value = this.value()

        for(let index=0index < repeatindex++)

            this.add(value)

        return this

    }

}

 

10-6) nullable 타입과 프로그램 안전성

(1) nullable 타입이란?

- 자바스크립트와 타입스크립트는 변수가 초기화되지 않으면 undefined라는 값을 기본으로 지정하는데, 사실상 같은 의미인 null이 존재한다.

- 타입스크립트에서 undefined값의 타입은 undefined이고, null값의 타입은 null이며 둘은 사실상 같은 것이므로 서로 호환이 된다.

- undefinednull 타입을 nullable타입이라고 하며, 코드로는 다음처럼 표현할 수 있다.

export type nullable = undefined | null

export const nullable: nullable = undefined

 

- nullable 타입들은 프로그램을 비정상으로 종료시키는 주요 원인이 되므로, 함수형 언어들은 이를 방지하기 위해 연산자나 클래스를 제공함.

 

(2) 옵션 체이닝 연산자

- 변수가 선언만 되고 어떤 값으로 초기화되지 않으면 런타임시 오류가 발생해 프로그램이 비정상적으로 종료되는데, 이를 방지하기 위해 옵션 체이닝연산자를 사용한다.

- 자바스크립트는 최근에 물음표 기호와 점 기호를 연이어 쓰는 ?. 연산자를 표준으로 채택하고 타입 스크립트는 버전 3.7.2 부터 이 연산자를 지원한다.

 

옵션 체이닝 연산자 사용 예

export type ICoordinates = {longitude: number}

export type ILocation = {country: stringcoords?: ICoordinates}

export type IPerson = {name: stringlocation?: ILocation}

 

let person: IPerson = {name: 'Jack'}

let longitude = person?.location?.coords?.longitude // safe navigation

console.log(longitude)

if(person && person.location && person.location.coords) {

    longitude = person.location.coords.longitude

}

 

(3) 널 병합 연산자

- 자바스크립트는 옵션 체이징 연산자를 표준으로 채택하면서 동시에 물음표 두 개를 연달아 이어 붙인 ?? ‘널 병합 연산자(nullish coalescing operator)’도 표준으로 채택했다.

- 옵션 체이닝 연산자 부분이 undefined가 되면 널 병합 연산자가 동작해 undefined 대신 0을 반환한다.

export type ICoordinates = {longitude: number}

export type ILocation = {country: stringcoords: ICoordinates}

export type IPerson = {name: stringlocation: ILocation}

 

let person: IPerson

 

// 병합 연산자를 사용해 기본값 0 설정

let longitude = person?.location?.coords?.longitude ?? 0

console.log(longitude)

 

 

  Comments,     Trackbacks