TypeScript 50 Lessons Part 3
타입스크립트 in 50 레슨
스테판 바움가트너가 스매싱 매거진에서 출판한 서적으로 타입스크립트 주요 주제들에 대해 잘 짚어주고 있다.
Lesson 15
타입스크립트는 값을 생성하는 영역과 타입을 생성하는 영역으로 구분, 함수는 값을 생성
함수는 선언부와 본문으로 구성된다. `declare` 키워드는 함수의 본문을 구현하지 않고 코드를 구성할 수 있게 해준다. 인자부와 반환부에 타입을 선언할 수 있다.
declare function search(query: string, tags?: string[]): Result[]
실제 구현을 하게 되면 이 함수는 비동기로 작동하는데 데이터 fetch 를 위해 `fetch` 함수를 사용하기로 가정하면, 자바스크립트의 fetch 는 `Promise<any>` 를 반환하기 때문에 이 값을 Result 타입으로 캐스팅하여 타입 세이프한 환경을 구성할 수 있다.
function search(query: string, tags?: string[]) {
//...
return fetch(`/search${queryString}`)
.then(response => response.json() as Promise<Result[]>)
}
`as` 키워드는 좌측의 값을 우측의 타입으로 처리한다. 함수 선언부에 반환 정보를 정의하는 것과 같은 역할을 한다.
function search(query: string, tags?: string[]): Promise<Result[]> {
//...
return fetch(`/search${queryString}`)
.then(response => response.json())
}
저자는 함수 선언부에 사용하는 것을 선호한다.
Lesson 16
함수 인자의 콜백 함수에 타입을 정의하여 타입 시스템을 강화
`typeof` 연산을 함수에 적용하면 함수의 타입을 알 수 있다.
type SearchFn = typeof search
type SearchFn = (query: string, tags?: string[] | undefined) => Promise<Result[]>
함수 인자를 `Query` 라는 타입으로 정의해 보자.
type Query = {
query: string, tags?: string[],
assemble: (includeTags: boolean) => string
}
'assemble' 인자는 함수 타입을 가지고 이 함수는 includeTags 를 받아서 string 을 반환하는 콜백 함수인 것을 알 수 있다. 물론 이렇게 개선할수도 있다.
type AssembleFn = (includeTags: boolean) => string
type Query = { query: string, tags?: string[], assemble: AssembleFn }
콜백 함수를 사용하는 코드는 특정 기능을 끼워넣을 수 있는 함수로 패턴화 된다. 브라우저의 특정 엘리먼트에서 값을 받아 다른 엘리먼트로 노출하는 함수를 정의한다면 아래와 같이 확장이 가능하다.
declare function displaySearch(
inputId: string, outputId: string, search: SearchFn
): void
이 함수는 반환하는 값이 없다. (그냥 바깥 세상에 사이드이펙트만 발생시킨다.) `void` 는 lesson 17 에서 조금 더 알아본다.
`구조적 타입 시스템` 에서 함수 인자의 타입 정보는 이름에 제약되지 않고 인자의 전달 순서에 따라 제약된다. 아래 두 함수는 같은 타입 정보를 가진다.
const testSearch: SearchFn = function(query, tags) {
return Promise.resolve([{
title: `The ${query} test book`,
url: `/${query}-design-patterns`,
abstract: `A practical book on ${query}`
}])
}
const testSearch: SearchFn = function(term, options) {
return Promise.resolve([{
title: `The ${term} test book`,
url: `/${term}-design-patterns`,
abstract: `A practical book on ${term}`
}])
}
위 코드의 경우 'tags' 나 'options' 는 사용되지 않고 있다. 구조적 타입 시스템에서 이 두 번째 인자를 생략해도 타입 점검을 정상적으로 완료한다. 이어지는 레슨에서 이 타입들을 좀 더 다듬어 볼 예정이다.
Lesson 17
대체가능성
이전 코드의 함수 본문에서 첫 번째 인자도 사용하지 않는다면 선언하지 않아도 된다.
const dummyContentSearchFn: SearchFn = function() {
return Promise.resolve([{
title: 'Form Design Patterns', url: '/form-design-patterns',
abstract: 'A practical book on accessible forms'
}])
}
자바스크립트에서 함수는 인자의 개수에 따라 주의해야할 케이스가 있다. 필요한 인자가 없는 경우와 인자가 너무 많은 경우인데 함수를 실행할 때 필요한 인자가 없는 경우 런타임에 실행 실패한다. 인자가 너무 많은 경우 초과된 인자 그냥 무시된다. 이런 오류들은 타입스크립트를 적용하면 사전 점검된다.
'SearchFn' 을 사용하는 displaySearch 함수의 인자로 콜백 함수를 전달하는데 SearchFn 의 모양을 가지면 사용할 수 있다.
displaySearch('input', 'output', dummyContentSearchFn)
이를 대체 가능성이라 한다.
대체 가능성은 반환하는 값의 타입이 일치하면 이루어진다. 위 코드의 경우 두 콜백 함수 모두 배열 결과를 담은 promise 함수를 반환하고 있다.
타입스크립트의 `void` 는 다른 프로그래밍 언어에서 사용하는 void 와 조금 다르다. 자바스크립트의 모든 함수는 기본적으로 undefined 를 반환한다. 타입스크립트에서도 모든 함수는 반환 타입이 있다. 타입 추론이 어려울 경우 기본적으로 `void` 타입을 반환한다. void 타입의 하나의 값이 있는데 그 값은 'undefined' 이다.
void 타입은 모든 반환 타입에 대해 대체 가능하다. 콜백 함수가 void 반환인 경우 number 를 반환해도 타입 점검은 성공한다. 하지만 실제 반환되는 타입은 undefined 이기 때문에 다른 연산은 할 수 없다.
function search(query: string,
callback: (results: Result[]) => void,
tags?: string[]) {
fetch(`/search${queryString}`)
.then(res => res.json() as Promise<Result[]>)
.then(results => {
const didItWork = callback(results)
didItWork += 2
})
}
이 경우 didItWork 는 undefined 를 가지기 때문에 컴파일에 실패한다. 바닐라 자바스크립트에서도 void 를 사용하여 함수의 실행 결과를 undefined 시킬 수 있다.
함수의 반환 타입을 `void` 에서 `undefined` 로 변경하면 대체 가능성을 제거할 수 있다.
callback: (results: Result[]) => undefined,
그리고 옵셔널 파라미터는 가장 마지막에 전달하는게 관행이다.
Lesson 18
콜백 함수의 첫 인자에 명시적으로 this 를 추가하고 타입을 부여할 수 있다.
displaySearch 함수를 작성하는 과정에 this 를 사용하게 되는데 this.value 같은 경우 타입 추론이 실패하여 컴파일이 되지 않는다.
function displaySearch(inputId: string, outputId: string, search: SearchFn): void {
document.getElementById(inputId)?.addEventListener('change', function () {
this.parentElement?.classList.add('active')
const searchTerm = this.value
})
}
DOM 엘리먼트도 타입스크립트를 위해 타입을 제공하고 있다. 타입을 좁혀 타입스크립트 컴파일러가 추론이 가능하도록 개선해 본다.
this.parentElement?.classList.add('active')
if (this instanceof HTMLInputElement) {
const searchTerm = this.value
}
이어서 이 addEventListner 의 콜백을 별도의 함수로 분리하면 this 는 콜백 함수의 첫 번째 인자로 this 의 타입을 정의해 줄 수 있다.
function inputChangeHandler(this: HTMLElement) {
this.parentElement?.classList.add('active')
}
이 인자는 타입스크립트만을 위한 정보로 컴파일된 자바스크립트에는 사라지게된다.
function inputChangeHandler() {
this.parentElement?.classList.add('active');
}
그리고 this 를 포함한 함수는 호출되는 컨텍스트에 따라 달라지기 때문에 this 를 HTMLElement 타입으로 추론할 수 없는 위치에서 호출하게 되면 컴파일 되지 않는다.
Lesson 19
함수 타입의 추가 기능들
tagged template 함수는 함수 호출에 '(인자)' 대신 '`인자`' 를 사용한다. 태그드 템플릿은 템플릿 문자 배열과 대상이 되는 문자열 표현식으로 구분된다.
taggedTemplate`템플릿A${표현식1}템플릿B`
taggedTemplate`템플릿A${표현식1}템플릿B${표현식2}템플릿C`
taggedTemplate`템플릿A${표현식1}템플릿B${표현식2}템플릿C${표현식3}템플릿D`
템플릿과 표현식은 배열로 처리되는데 표현식은 rest 연산자를 통해 표현된다.
function taggedTemplate(term: string, ...tags: string[]): Promise<Result[]>
Promise 를 반환하는 경우 async 로 함수를 선언하고 await 를 통해 Promise 의 resolved 값을 받을 수 있다.
Lesson 20
함수 오버로딩
타입스크립트는 함수 인자의 개수와 인자/반환 타입이 달라지는 경우를 위해 함수 오버로딩을 지원한다.
function search(term: string, tags?: string[]): Promise<Result[]>
function search(term: string, callback: (results: Result[]) => void, tags?: string[]): void
이 경우 실제 구현 형태는 이렇게 풀이된다.
function search(term: string, p2?: string[] | ((results: Result[]) => void), p3?: string[])
처음으로 `|` 를 통한 유니언 타입이 소개되고 있다. 이 함수를 타입으로 등록하면 좀 더 나은 코드를 볼 수 있다.
type SearchOverload = {
(term: string, tags?: string[] | undefined): Promise<Result[]>
(term: string, callback: (results: Result[]) => void, tags?: string[] | undefined): void
}
const search: SearchOverload = (term: string, p2?: string[] | ((results: Result[]) => void), p3?: string[]) => {
// body
}
Lesson 21
제너레이터
제너레이터는 이터레이션을 구현하기 위해 시간의 흐름에 따라 값을 생성해 제공해 준다. 타입스크립트는 다양한 타입 정보를 제공하여 제너레이터 함수를 손쉽게 사용할 수 있도록 해준다. 제너레이터 함수의 반환 타입은 아래와 유사하다. async 함수가 반환하는 Promise 와 비교해 보면 도움이 될 것이다.
Generator<1 | 2 | 3 | 4, string, unknown>
제너레이터는 풀링이 필요한 시스템에서 유용하다. fetch 를 통해 데이터를 받도록 하자.
type PollingResults = { results: Result[]; done: boolean }
async function polling(term: string): Promise<PollingResults> {
return fetch('/...').then((res) => res.json())
}
입력이 있을 때마다 백엔드에서 쿼리를 조회하는 제너레이터 함수는 이렇게 구성된다.
async function* getResults(term: string): AsyncGenerator<Result[], void, unknown> {
let state
do {
state = await polling(term)
yield state.results
} while (!state.done)
}
AsyncGenerator 타입은 타입스크립트의 시스템 인터페이스로 등록되어 있다.
let resultsGen = getResults(this.value);
for await(results of resultsGen) {
results.map(someFn)
}
제너레이터는 이터레이터이기 때문에 `for … of` 대상이 된다. yield 의 반환 값을 처리하기 위해 .next() 을 사용할 수 있다. `.next(value)` 를 통해 yield 의 반환 값을 받을 수 있다.
let state, stop
do {
state = await polling(term)
stop = yield state.results
} while (!state.done && !stop)
브라우저에서 입력된 값을 사용하는 제너레이터 호출 구문은 아래처럼 구성될 수 있다.
document.getElementById('searchField')?.addEventListener('change', handleChange)
async function handleChange(this: HTMLElement, ev: Event) {
if (this instanceof HTMLInputElement) {
let resultsGen = getResults(this.value)
let next, count = 0
do {
next = await resultsGen.next(count >= 5)
if (typeof next.value !== 'undefined') {
next.value.map(appendResultToAnswerArea)
count += next.value.length
}
} while (!next.done)
}
}
await 와 .next(isStop) 을 통해 비동기로 데이터를 요청하고 반환된 결과의 상태에 따라 응답읍 제공한다. 이 경우 AsynGenerator 의 타입은 이렇게 정의된다.
AsyncGenerator<Result[], void, boolean>