전체 코드는 git 에서 확인 가능합니다. 작성된 내용은 ts 공식문서를 학습하며 주관적으로 재구성한 내용으로, 오류가 있을 수 있습니다. 잘못된 내용은 댓글로 알려주시면 감사히 수정하겠습니다 : )
여기 배열의 첫 번째 원소를 반환하는 함수가 있다.
function firstElement(arr){
return arr[0]
}
이 함수는 파라미터와 리턴값에 타입을 지정하지 않았으니, any
로 간주된다.
이제 타입을 지정해 보자. 일단 숫자로 이루어진 배열의 원소를 얻고 싶어서, 파라미터와 리턴의 타입을 각각 number[]
와 number
로 지정한다.
근데 숫자 배열 말고, 문자 배열도 저 함수에 넣고 싶어서 유니온을 사용하여 파라미터 타입을 추가 했다.
그런데! 이 함수는 모든 배열, 그리고 문자열, iterable 한 객체가 모두 가능하다! 그렇다면 이러한 타입들을 위해서 유니온 타입으로 아래처럼 괴상하게 만들어야 할까?
function firstElement(arr: number[] | string[] | boolean[] | string): number | string | boolean{
return arr[0]
}
ts에서는 이러한 범용적인, Generic
한 함수를 좀 더 유연하고 재사용성 높게
사용하기 위해 특별한 방법을 제공한다. 또한, 입력값과 출력값의 관계를 표현할 수 있게 한다. 함수의 입력값과 출력값의 타입을 함수를 정의할 때
가 아닌, 사용할 때
결정하게 하는 것이다.
Generic
을 사용하는 이유?
- 유연한 사용, 재사용성을 높인다.
- 입력값과 출력값의 타입을 연관지을 수 있다.(관계성을 표현할 수 있다)
사용방법은 다음과 같다. Type
은 파라미터의 타입으로 그냥 이름이다. 공식문서에는 Type
으로 써있지만, 실제로 사용할 때는 줄여서 T
로도 많이 쓰는 것 같다.
function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0];
}
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
위 처럼 일반 함수를 정의하고, 사용할 때는 타입을 명시해주는 것이다. 그러면 입력한 타입에 맞게 반환값도 ts가 추론을 잘 할 수 있다.
그리고 ts는 굳이 타입을 명시하지 않아도, 파라미터를 보고 타입을 알아서 추론하기 때문에 <Type>
은 생략 가능하다.
// s is of type 'string'
const s = firstElement(["a", "b", "c"]);
// n is of type 'number'
const n = firstElement([1, 2, 3]);
// u is of type undefined
const u = firstElement([]);
그리고 둘 이상의 파라미터를 사용하는 경우에는 꺽쇠 안의 타입 명칭을 나열하여 사용하면 된다.
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
return arr.map(func);
}
// Parameter 'n' is of type 'string'
// 'parsed' is of type 'number[]'
const parsed = map(["1", "2", "3"], (n) => parseInt(n));
어떤 함수 중에서는 특정한 속성을 가져야 하는 등, 특정 타입(혹은 집합)의 하위 집합에서만 작동할 수 있다. 예를 들면 다음의 함수는 입력값의 길이
를 리턴한다.
function getLength<T>(arr: T): number {
return arr.length
}
그러려면 입력값에는 length
라는 프로퍼티가 있어야만 한다. 그런데 저 타입에 길이 속성이 있다는 보장이 없으므로, ts는 "length 속성이 없잖슴~" 이라고 한다.
이러한 경우는 해결하기 위해 ts는 extends
라는 키워드를 제공한다. 사용법은 다음과 같다. 이제부터 타입은 그냥 냅다 타입이 아니라, {length: number}
를 확장한 모습이여야 한다고 제한하는 것이다.
function getLength<T extends {length: number} >(arr: T): number {
return arr.length
}
ts는 일반적으로 제네릭 함수에서 의도한 타입을 유추 할 수 있지만, 항상 그렇지는 않다. 그래서 다음과 같은 경우에 arr1, arr2가 동일한 타입으로만 올 것으로 예상하고 오류가 발생한다.
위 경우에는, 함수를 사용하는 부분에서 타입을 생략하지 않고 수동으로 지정하면 의도대로 사용할 수 있다.
const arr2 = combine<string | number>([1, 2, 3], ["hello"]);
타입 파라미터를 아래에다 넣어라? 무슨 말인지 모르겠으나, 아웃풋 타입을 명확하게 할 수 있는 게 좋다는 것 같다.
function firstElement1<Type>(arr: Type[]) {
return arr[0];
}
function firstElement2<Type extends any[]>(arr: Type) {
return arr[0];
}
// a: number (good)
const a = firstElement1([1, 2, 3]);
// b: any (bad)
const b = firstElement2([1, 2, 3]);
다음 두 함수는 완전히 동일한 일을 하는 함수다. 후자는 타입 파라미터로 Type
과 Func
두 개를 썼는데, Func
는 반환값과 별다른 연관이 있지도 않고, 쉽게 말해 굳이..? 라는 것이다. 이러한 방법은 함수를 읽고 추론하기 어렵게 만들 뿐이다.
// good
function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
return arr.filter(func);
}
// bad...
function filter2<Type, Func extends (arg: Type) => boolean>(
arr: Type[],
func: Func
): Type[] {
return arr.filter(func);
}
타입 파라미터는 기본적으로 인풋과 아웃풋의 관계를 나타내기 위한 것이다. 그래서 최소 인풋에 한번, 아웃풋에 한번 총 두번 등장해야하는데, 다음 예제에서는 한번 뿐이다. 리턴 값이 없기 때문!
function greet<Str extends string>(s: Str) {
console.log("Hello, " + s);
}
greet("world");
위 함수는 굳이 제네릭으로 쓰일 필요 없이 간단하게 아래처럼 작성 할 수 있다.
function greet(s: string) {
console.log("Hello, " + s);
}
ts에서는 함수에 필수가 아닌, 선택적 파라미터를 ?
를 사용하여 표시할 수 있다. 혹은 default 값을 지정할 수도 있다.
// 선택적 매개변수
function f(x?: number) {
// ...
}
f(); // OK
f(10); // OK
// default
function f1(x = 1) {
// ...
}
타입을 명시하면 다음과 같이 사용할 수 있다. 선택적 매개변수이기때문에, 비우거나 undefined
도 가능하다.
function f2(x?: number): void {
//
}
f2()
f2(1)
f2(undefined)
함수 오버로드는, 실제 함수를 정의 한 것 위에 같은 이름의 다른 파라미터를 가진 서명을 추가하는 것이다. 이렇게 작성하면, 마지막에 실제로 정의한 파라미터 대신 위에 오버로드로 작성한 파라미터들을 사용할 수 있다.
첫번째 오버로드 함수는 파라미터를 한개, 두번째 오버로드 함수는 파라미터는 3개 받는다. 그래서 함수를 실제 정의할 때는 첫번째는 필수, 두세번째는 선택으로 작성하였다.
function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
if (d !== undefined && y !== undefined) {
return new Date(y, mOrTimestamp, d);
} else {
return new Date(mOrTimestamp);
}
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3); // No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.
마지막 함수 정의부분만 있다면 d3
에 오류가 없어야 하지만, 오버로드한 함수 두개는 1개 아니면 3개의 파라미터를 받아야하기 때문에 오류가 발생한다. 함수 본문을 작성하는데 사용된 시그니처는 외부에서 볼 수 없기 때문이다.
구현의 서명은 외부에서 볼 수 없습니다. 오버로드된 함수를 작성할 때 함수 구현 위에 항상 두 개 이상의 서명이 있어야 한다.
function fn(x: string): void;
function fn() {
// ...
}
// Expected to be able to call with zero arguments
fn(); // Expected 1 arguments, but got 0.
그리고 구현 서명은 오버로드의 서명과 항상 호환
되어야 한다.
function fn(x: boolean): void;
// Argument type isn't right
function fn(x: string): void; // This overload signature is not compatible with its implementation signature.
function fn(x: boolean) {}
즉, 구현의 서명 부분이 오버로드를 포괄해야 한다.
제네릭과 마찬가지로 오버로드를 사용할 때 따라야 하는 몇가지 지침이 있다.
function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {
return x.length;
}
위 함수는 문자열 또는 배열을 받아 그 길이를 반환하는 함수이다. 이 함수는 문제 없이 괜찮은 함수이다.
하지만 오버로드와 유니온 타입은 다르다. 각각 다른 타입을 받는 함수 두개를 분리하면 ts는 단일 오버로드에 대한 함수 호출로만 해결할 수 있기 때문에 둘 중 무엇일지 모르는 것은 사용할 수 없다.
이러한 경우에는 오버로드 대신 유니온 타입으로 바꾸면 문제를 해결할 수 있다.
- 제네릭 함수는 인풋, 아웃풋의 관계를 나타내고 함수의 확장성(범용성)을 높이기 위해 사용한다.
- 반대로, 인풋-아웃풋의 관계성을 굳이 나타낼 필요가 없으면 안써도 된다.