타입스크립트를 사용할 때, 함수에서 인자값을 넣을 수도 안 넣을수도 있게끔 하려면 어떻게 해야 할까요?
const hello = (name: string | null = null): void => {
console.log(`hello ${name}`)
}
hello()
hello("admin")
먼저 이렇게 유니온 타입을 활용해서 디폴트값으로 null이나 undefined를 대입시키는 방법이 있습니다
혹은 선택적 매개변수(optional parameter, 값이 없으면 undefined)를 이용하는 방법도 있구요
const hello = (name?: string): void => {
console.log(`hello ${name}`)
}
*인자가 없을 경우 "hello undefined"라는 문자열이 출력됩니다
아래의 reverse 함수는 문자열, 혹은 숫자를 인자로 받았을 때 그 순서를 뒤집는 기능을 합니다
const reverse = (x: number | string): string | number => {
const result = x.toString().split("").reverse().join("")
return (typeof x === "number") ? parseInt(result) : result
}
console.log(reverse(123), reverse("abc"))
당장은 결과가 잘 출력되는 것 같지만,
위 코드는 타입추론상 result의 타입이 number일 수도, string일 수도 있기 때문에
result에 대해 공통적으로 사용되는 메서드만 사용할 수 있다는 문제가 발생합니다
그러면 인자가 number면 리턴값도 number를, string이면 string을 반환받으려면 어떻게 해야 할까요
우선 함수 오버로드를 사용하는 방법이 있습니다
함수 오버로딩(overloading)이란?
타입스크립트에서는 같은 이름을 가진 함수를 여러 개 정의할 수 있으며,
이 때 각 함수는 서로 다른 타입을 가지는 매개변수로 정의해야 합니다
이와 같이 매개변수가 다르지만 이름이 동일한 함수를 여러개 생성하는 것을 함수 오버로딩이라고 합니다
// 함수 오버로드
// 표현식
function reverse(x: string): string
function reverse(x: number): number
function reverse(x: string | number): string | number {
const result = x.toString().split("").reverse().join("")
return (typeof x === "number") ? parseInt(result) : result
}
// 화살표 문법은??
// const reverse = (x: number| string): string|number => {
// const result = x.toString().split("").reverse().join("")
// return (typeof x === "number") ? parseInt(result) : result
// }
const resultStr = reverse("abc")
const resultNum = reverse(123)
결과 인자의 타입과 리턴값의 타입이 일치하는 것을 확인할 수 있습니다
이처럼 함수 오버로드는 컴파일러에게 함수 호출 시에 매개변수와 리턴값의 타입을 추론하는데에 도움을 줄 수 있습니다
제네릭: 제네릭은 타입의 일반화를 가능하게 해줍니다
함수나 클래스에서 타입이 고정되지 않고 유연하게 대처할 수 있도록 합니다
제네릭은 함수나 클래스에서 사용되는 타입을 미리 정하지 않고,
호출되거나 인스턴스화될 때 타입을 동적으로 결정할 수 있도록 하는 기능입니다
함수 x 제네릭 : 함수 호출 시 전달되는 인자의 타입에 따라 리턴값의 타입이 결정됩니다
제네릭 타입은 일반적으로 T
라는 이름으로 표현되며, (특별한 의미는 없지만)
함수에서 T를 사용하여 입력된 인자의 타입과 리턴값의 타입을 추론합니다
클래스 × 제네릭 : 클래스가 인스턴스화될 때 제네릭 타입이 결정됩니다
클래스에서 제네릭 타입은 클래스 이름 뒤에 <T>
와 같은 형태로 선언됩니다
제네릭은 타입 안정성을 보장하는 데에도 도움이 됩니다
예를 들어서, 배열 요소의 타입을 일치하지 않는 값으로 설정하면
컴파일 과정에서 오류가 발생하지 않고 런타임으로 넘어가는데,
제네릭을 사용하면 컴파일 시에도 타입 안정성을 보장할 수 있습니다
아래는 제네릭에 대한 이해를 돕기 위한 예제 코드들입니다
예제 1)
// 1 -> 1
// 'a' -> 'a'
// {} -> {}
// [] -> []
const echo = (type: any): any => {
console.log(type)
}
echo(1)
echo('a')
echo({})
echo([])
any타입이 리턴값의 타입을 추론하지 못하는 문제를 제네릭을 사용하면 해결할 수 있습니다
// <T>(1): 타입 매개변수를 선언
// T(2) : 매개변수의 타입을 정의
// T(3) : 리턴값의 타입을 정의
const echo = <T>(type: T): T => {
console.log(type)
return type
}
echo(1)
echo('a')
echo({})
echo([])
인자의 타입에 따라 매개변수와 리턴값의 타입이 결정되었습니다
예제 2)
interface Props {
name: string
id: string
}
const props: Props = {
name: "kim",
id: "admin"
}
const echo = <T>(type: T): T => {
console.log(type)
return type
}
echo(props) // 타입추론 발동
echo<Props>(props) // 타입 명시
const push = <T>(arr: T): T[] => {
const result = [arr]
console.log(result)
return result
}
push(1) // number[]
push('a') // string[]
예제3)
좀 전의 함수 오버로드 예제에서 살펴본 이슈도 제네릭을 써서 해결할 수 있습니다
// 리턴 타입을 미리 명시합니다. 대신 number나 string 인자만 받을 수 있습니다
const reverse = <T>(x: T): number | string => {
// 어느정도 타입 추론이 되기는 하지만...
// x에 대한 타입을 as string으로 고정해주지 않으면 split 메서드에서 오류 발생
const params = (typeof x === "number") ? x.toString() : x as string
const result = params.split("").reverse().join("")
return (typeof x === "number") ? parseInt(result) : result
}
const resultStr = reverse("abc") // "cba"
const resultNum = reverse(123) // 321
// 위 코드를 개선한 버전
const reverse = <T>(x: T): T => {
const params = (typeof x === "number") ? x.toString() : x
const result = (typeof params === "string") ? params.split("").reverse().join("") : params
return result as T // 리턴 타입을 제네릭으로 형변환
}
const resultStr = reverse("abc") // "cba"
const resultNum = reverse(123) // 321
const resultObj = reverse({abc: "abc"}) // 객체나 배열 등은 원본 그대로 반환합니다
// 개선 버전2. 객체랑 배열도 뒤집어보기
const reverse2 = <T>(x: T): T => {
// 먼저 unknown을 할당합니다
let result: unknown = undefined
if (typeof x === "string") {
result = x.split("").reverse().join("")
} else if (typeof x === "number") {
result = x.toString().split("").reverse().join("")
} else if (x instanceof Array) {
result = x.reverse()
} else if (typeof x === "object" && x !== null) {
result = Object.fromEntries(Object.entries(x).reverse())
}
// 다시 제네릭 타입으로
return result as T
}
console.log(reverse2(123), reverse2("abc"), reverse2({ abc: "abc", 123: "123" }), reverse2([1, 2, 3]))
// 321 cba { '123': '123', abc: 'abc' } [ 3, 2, 1 ]
// 버전3. 버전2에 대한 리팩토링
const reverse2 = <T>(x: T): T => {
if (typeof x === "string" || typeof x === "number") {
return x.toString().split("").reverse().join("") as T
}
if (x instanceof Array) {
return x.reverse() as T
}
if (typeof x === "object" && x !== null) {
return Object.fromEntries(Object.entries(x).reverse()) as T
}
return null as T
}
console.log(reverse2(123), reverse2("abc"), reverse2({ abc: "abc", 123: "123" }), reverse2([1, 2, 3]))
// 321 cba { '123': '123', abc: 'abc' } [ 3, 2, 1 ]
// if 대신 스위치문을 쓸 수도 있겠네요
제네릭을 사용할 때는 어떠한 타입의 인자가 들어올 수도 있음을 가정하고 코드를 설계해야 합니다
그래서 함수 내부에서 if (typeof...)
나 스위치문을 자주 사용하게 됩니다