Ch10 제네릭 프로그래밍
10-1) 제네릭 타입 이해하기
- 제네릭 타입은 인터페이스나 클래스, 함수, 타입 별칭 등에 사용할 수 있는 기능으로, 해당 심벌의 타입을 미리 지정하지 않고 다양한 타입에 대응하려고 할 때 사용한다.
//제네릭 인터페이스 구문
Interface IValuable<T> {
Value: T
}
- 어떤 인터페이스가 value라는 이름의 속성을 가질 때, 속성의 타입을 다음처럼 string, number 등으로 특정하지 않고 T로 지정해 제네릭 타입으로 만들 수 있으며, 인터페이스 이름 뒤에 <T>로 표기한다.
(1) 제네릭 사용하기
- 제네릭 인터페이스 정의
export interface IValuable<T> {
value : T
}
- 제네릭 인터페이스 IValuable<T>를 구현하는 제네릭 클래스는 자신이 가진 타입 변수 T를 인터페이스 쪽 제네릭 타입 변수로 넘길 수 있다.
import {IValuable} from './IValuable'
export class Valuable<T> implements IValuable<T> {
constructor(public value: T) {}
}
export {IValuable}
- IValuable<T>, Valuable<T>를 사용하는 제네릭 함수는 다음처럼 자신의 타입 변수 T를 제네릭 인터페이스의 타입 변수 쪽으로 넘기는 형태로 구현할 수 있다.
import {IValuable, Valuable} from './Valuable'
export const printValue = <T>(o: IValuable<T>): void => console.log(o.value)
export {IValuable, Valuable}
※ 제네릭함수 사용 예시
import {printValue, Valuable} from './printValue'
printValue(new Valuable<number>(1))
printValue(new Valuable<boolean>(true))
printValue(new Valuable<string>('hello'))
printValue(new Valuable<number[]>([1, 2, 3]))
10-2) 제네릭 타입 제약
- 프로그래밍 언어에서 제네릭 타입 제약(generic type constraint)은 타입 변수에 적용할 수 있는 타입의 범위를 한정하는 기능을 한다.
<최종타입1 extend 타입1, 최종 타입2 extend 타입2>(a: 최종 타입1, b: 최종 타입2, …) {}
※ 타입 제약 구문 예시
import {IValuable} from './IValuable'
export const printValueT = <Q, T extends IValuable<Q>>(o: T) => console.log(o.value)
export {IValuable}
- 이때 주의해야할 점은
printValueT 를 <T extnds IValuable<T>>(o: T) 이런식으로 구현하면 안된다.
그 이유는 매개변수 o의 T입장에서 타입 T는 IValuable<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 = (obj, keys) => keys.map(key => ({[key]: obj[key]}))
.reduce((result, value) => ({...result, ...value}), {})
import {pick} from './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 = <T, K extends keyof T>(obj: T, keys: K[]) =>
keys.map(key => ({[key]: obj[key]}))
.reduce((result, value) => ({...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 = <T, U>(a: T, b: U): T & U => ({...a, ...b})
import {mergeObjects} from './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: number, height: 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: number, height: number}
export interface ICircle {tag: 'circle', radius: number}
export type IShape = ISquare | IRectangle | ICircle
- switch문을 사용해 tag 값이 무엇이냐에 따라 적용될 함수를 구분
import {IShape} from './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 {Bird, Fish} from './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 {Bird, Fish} from './BirdAndFish'
export const isFlyable = (o: Bird | Fish): o is Bird => {
return o instanceof Bird
}
import {Bird, Fish} from './BirdAndFish'
export const isSwimmable = (o: Bird | Fish): o is Fish => {
return o instanceof Fish
}
- 단 사용자 정의 타입 가드 함수는 if문에서 사용해야한다.
※ 사용자 정의 타입 가드함수 사용 예
import {Bird, Fish} from './BirdAndFish'
import {isFlyable} from './isFlyable'
import {isSwimmable} from './isSwimmable'
export const swimOrFly = (o: Fish | Bird) => {
if(isSwimmable(o))
o.swim()
else if(isFlyable(o))
o.fly()
}
import {Bird, Fish} from './BirdAndFish'
import {swimOrFly} from './swimOrFly'
[new Bird, new 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 {IValueProvider} from '../interfaces'
export class Calculator implements IValueProvider<number> {
constructor(private _value: number = 0) {}
value(): number {return this._value}
}
- 같은 방식으로 StringComposer란 클래스로 IvalueProvider<T>를 구현
import {IValueProvider} from '../interfaces'
export class StringComposer implements IValueProvider<string> {
constructor(private _value: string = '') {}
value(): string {
return this._value
}
}
③ Iaddable<T>와 Imultiplyable<T> 인터페이스 구현
- Calculator의 add 메서드는 클래스의 this 값을 반환하며, 이는 메서드 체인(method chain)을 구현하기 위함이며, multiply도 마찬가지임
import {IValueProvider, IAddable, IMultiplyable} from '../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 * value; return this
}
}
※ 테스트
import {Calculator} from '../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 {IValueProvider, IAddable, IMultiplyable} from '../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=0; index < repeat; index++)
this.add(value)
return this
}
}
10-6) nullable 타입과 프로그램 안전성
(1) nullable 타입이란?
- 자바스크립트와 타입스크립트는 변수가 초기화되지 않으면 undefined라는 값을 기본으로 지정하는데, 사실상 같은 의미인 null이 존재한다.
- 타입스크립트에서 undefined값의 타입은 undefined이고, null값의 타입은 null이며 둘은 사실상 같은 것이므로 서로 호환이 된다.
- undefined와 null 타입을 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: string, coords?: ICoordinates}
export type IPerson = {name: string, location?: 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: string, coords: ICoordinates}
export type IPerson = {name: string, location: ILocation}
let person: IPerson
//널 병합 연산자를 사용해 기본값 0을 설정
let longitude = person?.location?.coords?.longitude ?? 0
console.log(longitude)
'TypeScript' 카테고리의 다른 글
Do it! 타입스크립트 프로그래밍 11장 (0) | 2020.05.23 |
---|---|
Do it! 타입스크립트 프로그래밍 9장 (0) | 2020.05.20 |
Do it! 타입스크립트 프로그래밍 8장 (0) | 2020.05.13 |
Do it! 타입스크립트 프로그래밍 7장 (0) | 2020.05.07 |
Do it! 타입스크립트 프로그래밍 6장 (0) | 2020.05.06 |