Typescript - 배열, 튜플
in DEV on Typescript
이 포스트는 함수형 프로그래밍의 일부인 선언형 프로그래밍과 함수형 프로그래밍을 가능하게 하는 순수 함수를 다루면서 배열과 튜플에 대해 알아본다.
소스는 assu10/typescript.git 에 있습니다.
1. 배열
자바스크립트에서 배열은 다른 언어와 다르게 객체이다.
배열은 Array 클래스의 인스턴스인데 클래스의 인스턴스는 객체이기 때문이다.
Array.isArray()
는 전달받은 심벌이 배열인지 객체인지 알려주는 역할을 한다.
// 배열
let a = [1, 2, 3]
let o = {name: 'assu'}
console.log(Array.isArray(a), Array.isArray(o)) // true, false
배열의 타입은 아이템 타입[]
이다.
// 배열의 타입
let numArr: number[] = [1, 2, 3]
type IPerson = {name: string, age?: number}
let personArr: IPerson[] = [{name: 'assu'}, {name: 'jhlee', age:20}]
console.log(numArr); // [ 1, 2, 3 ]
console.log(personArr); // [ { name: 'assu' }, { name: 'jhlee', age: 20 } ]
1.1. split, join
다른 언어에서는 문자열(string) 을 문자(character) 들의 배열로 간주하지만 타입스크립트에서는 문자 타입이 없기 때문에 문자열을 가공하려면 문자열을 배열로 전환해야 한다.
String 클래스의 split 메서드
를 이용하여 문자열을 배열로 만들 수 있다.
string[] 타입의 배열을 다시 string 으로 변경 시엔 Array 클래스의 join 메서드
를 사용한다.
split(구분자: string): string[]
join(구분자: string): string
// split, join
console.log('hello'.split('')) // [ 'h', 'e', 'l', 'l', 'o' ]
console.log('h_e_l_l_o'.split('_')) // [ 'h', 'e', 'l', 'l', 'o' ]
console.log([ 'h', 'e', 'l', 'l', 'o' ].join()) // h,e,l,l,o
console.log([ 'h', 'e', 'l', 'l', 'o' ].join('')) // hello
console.log([ 'h', 'e', 'l', 'l', 'o' ].join('_')) // h_e_l_l_o
1.2. 배열의 비구조화 (잔여 연산자와 전개 연산자, ...
)
// 배열의 비구조화
// 잔여 연산자
let array = [1,2,3,4,5]
let [first, second, third, ...rest] = array
console.log(first, second, third, rest) // 1 2 3 [ 4, 5 ]
// 전개 연산자
let arr1: number[] = [1]
let arr2: number[] = [1,2,3]
let mergedArr: number[] = [...arr1, ...arr2, 4]
console.log(mergedArr) // [ 1, 1, 2, 3, 4 ]
1.3. for...in
, for...of
ESNext 자바스크립트와 타입스크립트에서 제공하는 반복문이다.
for...in
- 배열의 인덱스값을 대상으로 순회
for...of
- 배열의 아이템값을 대상으로 순회
즉, 타입에 무관하에 배열에 담긴 값을 차례로 얻음
- 배열의 아이템값을 대상으로 순회
for (let 변수 in 객체) {
}
for (let 변수 of 객체) {
}
// for...in, for...of
// 배열인 경우 for...in
let array = ['assu', 'jh', 'lee']
for (let index in array) {
const name = array[index]
console.log(`${index} - ${name}`)
}
// 객체인 경우 for...in
let obj = {name: 'assu', age: 20}
for (let prop in obj) {
console.log(`${prop} ${obj[prop]}`) // tsconfig.json 의 stric 이 false 이어야 함
}
// 배열인 경우 for...of
for (let name of array) {
console.log(name)
}
1.4. 제네릭 방식 타입
배열을 다루는 함수를 작성할 때 number[] 와 같이 타입이 고정된 함수보다는 T[]
형태로 배열의 아이템 타입을 한꺼번에 표현하는 것이 편리하다.
타입을 T
와 같은 일종의 변수(타입 변수)로 취급하는 것을 제네릭 타입
이라고 한다.
// 제네릭 방식 타입
let numArray: number[] = [1, 2, 3]
let strArray: string[] = ['one', 'two'];
type IPerson = {name: string, age?: number}
let personArray: IPerson[] = [{name: 'assu'}, {name: 'jhlee', age: 20}]
const arrayLength = (array: string[]) => array.length
const arrayGenericLength = <T>(array: T[]): number => array.length
const isEmpty = <T>(array: T[]): boolean => arrayGenericLength(array) == 0
console.log(
arrayGenericLength(numArray), // 3
arrayGenericLength(strArray), // 2
arrayGenericLength(personArray), // 2
isEmpty([]), // true
isEmpty([1]) // false
)
1.5. 제네릭 방식 타입 추론
제네릭 형태로 구현된 함수는 원칙적으로는 아래와 같이 명시해주어야 한다.
함수명<타입 변수>(매개 변수)
let fn = <T>(n: T): T => n
하지만 코드가 번거로워지므로 타입스트립트는 타입 변수 부분을 생략할 수 있게 해준다.
// 제네릭 방식 타입 추론
console.log(
fn<boolean>(true), // true
fn(true) // true
)
1.6. 전개 연산자를 사용하여 range 함수 구현
위의 1.2. 배열의 비구조화 (잔여 연산자와 전개 연산자, ...
) 에서 본 전개 연산자를 사용하면 ramda
를 사용하면 R.range
와 같은 함수를 쉽게 만들 수 있다.
// 전개 연산자를 사용하여 range 함수 구현
// 재귀 함수 스타일로 동작하도록 구현
const range = (from: number, to: number): number[] =>
from < to ? [from, ...range(from+1, to)]: []
let numbers: number[] = range(1, 9+1)
console.log(numbers) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
2. 배열의 filter
, map
, reduce
메서드
2.1. filter
메서드
filter
메서드는 배열 각 요소에 대하여 주어진 함수의 결과값이 true 인 요소를 모아 새로운 배열을 반환하는 메서드이다.
배열의 타입이 T 일 때 배열의 filter 메서드는 아래 형태로 설계되어있다.
filter(callback: (value: T, index?: number): boolean): T[]
filter
메서드를 이용하여 숫자 배열에서 홀수와 짝수를 구분하는 기능을 만들어보자.
filter
는 배열의 원본을 변경하지 않는다. (순수 함수)
순수 함수 는 바로 다음 부분에 설명이 있습니다.
// filter 메서드
import {range} from "./range";
const array: number[] = range(1, 10+1)
let odds: number[] = array.filter((value => value % 2 != 0))
let evens: number[] = array.filter((value => value % 2 == 0))
console.log(odds, evens) // [ 1, 3, 5, 7, 9 ] [ 2, 4, 6, 8, 10 ]
filter 는 두 번째 매개변수에 index
라는 선택 속성도 제공한다. 아래는 index 를 이용하여 배열을 반으로 나누는 기능이다.
// filter 메서드 - 배열 반으로 나누기
import {range} from "./range";
const array: number[] = range(1, 10+1)
const half = array.length / 2
let belowHalf: number[] = array.filter((value, index) => index < half)
let overHalf: number[] = array.filter((value, index) => index >= half)
console.log(belowHalf, overHalf) // [ 1, 2, 3, 4, 5 ] [ 6, 7, 8, 9, 10 ]
2.2. map
메서드
map
메서드는 배열 각 요소에 대하여 주어진 함수를 수행한 결과를 모아서 새로운 배열을 반환하는 메서드이다.
배열의 타입이 T[]
일 때 배열의 map
메서드는 아래의 형태이다.
map(callback: (value: T, index?: number): Q): Q[]
map
도 배열의 원본을 변경하지 않는다. (순수 함수)
// map 메서드 - 제곱 구하기
import {range} from "./range";
let squared: number[] = range(1, 5+1).map((value: number) => value * value)
console.log(squared) // [ 1, 4, 9, 16, 25 ]
filter
와 달리 map
은 입력 타입과 다른 타입의 배열을 만들 수 있다.
// map 메서드 - number[] 타입 배열을 string[] 타입 배열로 가공
import {range} from "./range";
let names: string[] = range(1, 5+1).map((value, index) => `[${index}]: ${value}`)
console.log(names) // [ '[0]: 1', '[1]: 2', '[2]: 3', '[3]: 4', '[4]: 5' ]
2.3. reduce
메서드
reduce
메서드는 배열 각 요소에 대하여 reduce 함수를 실행하고, map/filter 와 달리 배열이 아닌 하나의 결과값을 반환하는 메서드이다.
배열의 타입이 T[]
일 때 reduce
메서드는 아래의 형태이다.
reduce(callback: (result: T, value: T), initiaValue: T): T
reduce
도 배열의 원본을 변경하지 않는다. (순수 함수)
// reduce 메서드 - 1~100 더하기
import {range} from "./range";
let reduceSum: number = range(1, 100+1).reduce((result: number, value: number) => result + value, 0)
console.log(reduceSum) // 5050
// 곱하기는 0 을 곱하면 안되니까 두 번째 인수에 1을 넣어준다.
let reduceMultiply: number = range(1, 10+1).reduce((result: number, value: number) => result * value, 1)
console.log(reduceMultiply) // 3628800
3. 순수 함수와 배열
3.1. 순수 함수
순수 함수
란 side-effect 가 없는 함수를 말한다.
side-effect 란 함수가 가진 고유한 목적 이외의 다른 효과가 나타나는 것을 의미한다.
- 순수함수
- 함수 몸통에 입출력 관련 코드가 없음
- 함수 몸통에서 매개변수값을 변경시키지 않음 (= 매개변수는 readonly 형태로만 사용)
- 함수는 몸통에서 만들어진 결과를 즉시 반환
- 함수 내부에서 전역 변수나 정적 변수를 사용하지 않음
- 함수가 예외를 발생시키지 않음
- 함수가 콜백 함수로 구현되었거나, 함수 몸통에 콜백 함수를 사용하는 코드가 없음
- 함수 몸통에 Promise 와 같은 비동기 방식으로 동작하는 코드가 없음
예를 들어 아래와 같은 함수는 순수 함수이다.
function pure(a: number, b: number): number {
return a + b
}
아래는 매개변수로 전달받은 배열은 push 와 slice 로 변경하고 (= 매개변수가 readonly 형태로 동작하지 않으므로) g 라는 외부 변수를 사용하므로 순수 함수가 아니다.
let g = 10
function impure(array: number[]): void {
array.push(1)
array.slice(0, 1)
return g
}
3.1. 타입 수정자 readonly
타입스크립트는 순수 함수 구현을 위해 readonly
키워드를 제공한다.
// 타입 수정자 readonly
function pure(array: readonly number[]) {
array.push(1) // TS2339: Property 'push' does not exist on type 'readonly number[]'.
}
3.2. 깊은 복사, 얕은 복사
- 깊은 복사
- 대상 변수값이 바뀔 때 원본 변수값은 그대로 유지
- number, boolean
- 얕은 복사
- 대상 변수값이 바뀔 때 원본 변수값도 변경
- 객체, 배열
// 깊은 복사, 얕은 복사
// 깊은 복사
let original: number = 1
let copied = original
copied += 2
console.log(original, copied) // 1 3
// 얕은 복사
const originalArr = [1,2,3,4]
const shallowCopiedArr = originalArr
shallowCopiedArr[0] = 10
console.log(originalArr, shallowCopiedArr) // [ 10, 2, 3, 4 ] [ 10, 2, 3, 4 ]
근데 전개 연산자 ...
를 사용해서 배열을 복사하면 깊은 복사를 할 수 있다.
// 전개 연산자를 이용하여 배열 깊은 복사
// 깊은 복사
const array = [1,2,3,3,4,5]
const deepCopiedArray = [...array] // 전개연산자 사용
deepCopiedArray[0] = 10
console.log(array, deepCopiedArray) // [ 1, 2, 3, 3, 4, 5 ] [ 10, 2, 3, 3, 4, 5 ]
3.3. 순수 함수로 sort 메서드 구현 (원본 배열 유지하면서 sort)
Array 클래스의 sort
메서드는 배열을 오름차순/내림차순으로 정렬해주는데 이 때 배열의 원본 내용도 같이 변경이 된다. (= 얕은 복사)
순수 함수란 함수 몸통에서 매개변수값을 변경시키지 않는 것 이라고 하였다.
readonly
타입을 이용하여 입력 배열의 내용을 유지한 채 정렬할 수 있도록 (= 순수 함수가 될 수 있도록) 해보자.
먼저 아래는 Typescript - 함수, 메서드 의 화살표 함수(=>
)와 표현식 에서 본 화살표 함수 기본 형태이다.
const 함수명 = (매개변수: 타입, 매개변수: 타입[,...]): 반환타입 => 함수 몸통
// 순수 함수로 sort 메서드로 구현
const pureSort = <T>(array: readonly T[]): T[] => {
let deepCopied = [...array]
return deepCopied.sort()
}
const beforeArray: number[] = [1,5,4,3]
const afterArray = pureSort(beforeArray)
console.log(beforeArray, afterArray) // [ 1, 5, 4, 3 ] [ 1, 3, 4, 5 ]
3.4. 순수 함수로 splice 메서드 구현 (원본 배열 유지하면서 특정 아이템 삭제)
배열에서 특정 아이템 삭제 시 splice
메서드를 사용하는데 splice
는 원본 배열을 변경하므로 순수 함수에서는 사용할 수 없다.
이 때 filter
메서드를 이용하여 특정 아이템을 삭제할 수 있는데
배열이 제공하는 filter
, map
메서드는 sort
와 다르게 깊은 복사 형태(= 원본 배열을 유지)로 동작한다.
// 순수 함수로 splice 메서드 구현 (원본 배열 유지하면서 특정 아이템 삭제)
const pureDelete = <T>(array: readonly T[], cb: (value: T, index?: number) => boolean): T[] =>
array.filter((value, index) => cb(value, index) == true)
const mixedArray: object[] = [
[], {name: 'assu'}, {name: 'jhlee', age:20}, ['desc']
]
const arrayOnly: object[] = pureDelete(mixedArray, (value) => Array.isArray(value))
const objectOnly: object[] = pureDelete(mixedArray, (value) => !Array.isArray(value))
console.log('mixedArray', mixedArray) // [ [], { name: 'assu' }, { name: 'jhlee', age: 20 }, [ 'desc' ] ]
console.log('arrayOnly', arrayOnly) // [ [], [ 'desc' ] ]
console.log('objectOnly', objectOnly) // [ { name: 'assu' }, { name: 'jhlee', age: 20 } ]
3.5. 순수 함수로 가변 인수 함수 구현
함수를 호출할 때 전달하는 인수의 개수를 제한하지 않는 것을 가변 인수
라고 한다.
아래를 보면 mergeArray 함수는 각각 2개, 4개의 인수를 전달받는데 이런 방식으로 동작하는 함수를 가변 인수 함수
라고 한다.
// 순수 함수로 가변 인수 함수 구현
import { mergeArray } from "./mergeArray";
const mergedArray1: string[] = mergeArray(
['hello'], ['world']
)
console.log(mergedArray1) // [ 'hello', 'world' ]
const mergedArray2: number[] = mergeArray(
[1], [2,3], [3,4,5]
)
console.log(mergedArray2) // [ 1, 2, 3, 3, 4, 5 ]
가변 인수 함수
의 기본 형태는 아래와 같다.
const mergeArray = (...arrays) => { }
위에서 매개변수 arrays 앞의 ...
은 잔여나 전개 연산자가 아니라 가변 인수를 표현하는 구문이다.
잔여 연산자와 전개 연산자의 상세 내용은 Typescript - 객체, 타입 의 4.2. 잔여 연산자와 전개 연산자 (
...
) 를 참고하세요.
string[], number[] 타입 배열을 모두 받으려면 제네릭 타입으로 구현해야 하므로 아래와 같이 수정한다.
const mergeArray = <T>(...arrays) => { }
또한 전달받는 인수는 모두 배열이므로 매개변수인 arrays 의 타입을 배열의 배열로 선언한다.
const mergeArray = <T>(...arrays: T[][]) => { }
매개변수는 배열의 배열인 T[][] 이지만 출력은 배열이므로 T[] 형태의 배열을 반환하도록 한다.
const mergeArray = <T>(...arrays: T[][]): T[] => { }
마지막으로 mergeArray 함수를 순수 함수 로 구현하려면 매개 변수의 내용이 변경되지 말아야 하기 때문에 매개 변수 타잎 앞에 readonly
키워드를 넣어준다.
const mergeArray = <T>(...arrays: readonly T[][]): T[] => { }
위 내용을 바탕으로 mergeArray 함수를 구현하면 아래와 같다.
// 순수 함수로 가변 인수 함수 구현
export const mergeArray = <T>(...arrays: readonly T[][]): T[] => {
let result: T[] = []
for (let index=0; index<arrays.length; index++) {
const array: T[] = arrays[index]
// result 와 array 배열을 각각 전개(spread)하고 결합(merge)해야 T[] 타입 배열 생성 가능
result = [...result, ...array]
}
return result
}
4. 튜플
자바스크립트에는 튜플
이 없으며 단순히 배열의 한 종류이다.
아래는 여러 타입에 대응하는 any
타입 배열을 선언한 예시이다.
let tuple: any[] = [true, 'hello']
그런데 any
타입의 형태는 타입스크립트의 타입 기능을 무력화하기 때문에 타입스크립트는 튜플의 타입 표기법을 배열과 다르게 선언할 수 있다.
let numArray: number[] = [1, 2, 3, 4]
let tuple: [boolean, string] = [true, 'hello']
4.1. 튜플에 타입 별칭 사용, 비구조화 할당 사용
튜플을 사용할 때는 타입 별칭으로 튜플의 의미를 명확하게 한다.
예를 들어 바로 위처럼 [boolean, string] 으로 타입을 지정하는 것보다 아래처럼 타입 별칭을 사용하여 이 튜플이 어떠한 용도로 사용되는지 분명하게 알려주는 것이 좋다.
// 튜플 - 타입 별칭 사용 (ResultType.ts)
export type ResultType = [boolean, string]
// 튜플 - 타입 별칭 (doSomething.ts)
import { ResultType } from "./ResultType";
// 예외 발생 시 구체적인 내용을 튜플로 반환
export const doSomething = (): ResultType => {
try {
throw new Error('Error occurs...')
} catch (e) {
if (e instanceof Error) {
return [false, e.message]
} else {
return [false, 'unknown error']
}
}
}
// 튜플 - 튜플에 비구조화 할당 적용 (index.ts)
import { doSomething } from "./doSomething";
const [result, errorMsg] = doSomething()
console.log(result, errorMsg) // false Error occurs...
본 포스트는 전예홍 저자의 Do it! 타입스크립트 프로그래밍을 기반으로 스터디하며 정리한 내용들입니다.