[TS] FUNCTIONS

yongkini ·2022년 5월 15일
0

TypeScript

목록 보기
6/11

Call Signatures

: JS의 함수는 일급 객체로 변수에 할당할 수 있었는데 그것처럼 함수도 타입을 미리 지정해서 type alias에 저장할 수 있다. 본래 이와 같이해야하는 것을아래와 같이 해줄 수 있다.아직 이것에 대한 실용성(?)은 제대로 알지 못하지만 나중에 props로 함수를 내려줄 때 쓴다고 한다. call signatures를 한마디로 정의하면 함수를 어떻게 호출하느냐를 정의해놓은 타입이라고 생각하면 된다. 매개변수에 어떤 타입을 넣어주고, 리턴 타입은 뭔지를 정의해주는 것!. 나중에 실제 예시를 배우게 되면 또다시 포스팅해보자. 지금은 이런게 있다고 알고 넘어가는 정도로 pass.

Overloading(?)

: 결론적으로 오버로딩은 위에서 언급한 call signature가 두개이상 있는(여러개 있는) 함수를 오버로딩했다고 표현할 수 있다. 예를 들어서, 매개변수의 개수는 똑같은데 어쩔 때는 string 타입의 매개변수가 들어오고, 어쩔때는 객체가 들어오는 경우가 있다고 했을 때, 이런 경우에 overloaing을 해서 call signature를 두개 세팅해준다. 위와 같은 예시를 들 수 있다. 위를 보면 pushToPath라는 함수는 떄에 따라 객체를 받기도 하고, string을 받기도하는데 이 때, call signature를 두개 써줘서(오버로딩해서) 각각의 케이스를 정의해줬고, 실제 함수 내에서도 그에 대한 분기처리를 해줘서 문제가 일어나지 않는다.
코드 복붙

type PathObj = {
	pathName: string,
    prevPathName: string,
    nowPathName: string
}

type Path = {
	(pathName: string) : void,
    (path: PathObj) : void
}

const pushToPath: Path = (param) => {
    if(typeof param === 'string') {
        console.log(`pushed to paths name : ${param}`); 
    }else {
        console.log(`pushed to paths name : ${param.pathName}`); 
    }
}

const param1:PathObj = {
    pathName:"yongki home",
    prevPathName: 'mother home',
    nowPathName : 'pangyo'
}

pushToPath(param1);

하지만 만약

type overloaded = {
    (a:number, b:number) : number,
    (a:number, b:number, c:number) : number
}

const addNums:overloaded = (a,b,c) => {
    return a+b+c;
}

위와 같은 상황이 생긴다면 따로 처리를 해줘야한다(에러를 일으킨다). 위에 매개변수가 3개인 경우는 옵션과 같으므로 이경우에는 c에 대해서 처리를 해줘야한다(지난 시간에 배운 옵셔널 타입을 써준다)

type overloaded = {
    (a:number, b:number) : number,
    (a:number, b:number, c:number) : number
}

const addNums:overloaded = (a,b,c?:number) => {
    if(c) return a+b+c;
    else return a+b;
}

Polymorphism(다형성) with generic(제네릭)

type overloaded = {
    (arr: number[]) : void,
    (arr: string[]) : void,
    (arr: boolean[]) : void,
}

const addNums:overloaded = (arr) => {
   arr.map(el => console.log(el));
}

addNums([1,2,3,4]);
addNums([true, false, true, false]);
addNums(["1","2","3","4"]);
addNums([false,"2",1,"4"]); // 이렇게 쓰려면?

위와 같은 상황을 가정해보자. 이 때, 마지막 코드에서는 에러가 난다. 그 이유는 call signature에 정의해둔 것중 어느 것에도 해당하지 않는 방식으로 함수를 호출했기 때문이다. 그래서 저부분에서 에러가 나지 않게하려면

type overloaded = {
    (arr: number[]) : void,
    (arr: string[]) : void,
    (arr: boolean[]) : void,
    (arr: (boolean|string|number)[]) : void,
}

이렇게 조건을 추가해줘야한다. 하지만 매번 이렇게 추가해주기 번거롭고, 들어오는 모든 타입에 대해서 타입스크립트 스스로 타입추론을 하여 쓸 수 있도록 쓰게 만드려면?? => 제네릭을 사용하면 된다.

type overloaded = {
    <myGeneric>(arr: myGeneric[]) : void,
    // (arr: string[]) : void,
    // (arr: boolean[]) : void,
    // (arr: (boolean|string|number)[]) : void,
}

const addNums:overloaded = (arr) => {
   arr.map(el => console.log(el));
}

addNums([1,2,3,4]);
addNums([true, false, true, false]);
addNums(["1","2","3","4"]);
addNums([false,"2",1,"4"]); // 이렇게 쓰려면?

실제로 위와 같은 방법으로 제네릭을 써주면 에러가 일어나지 않는다. 그리고 아랫부분에 커서를 올려보면 타입스크립트가 알아서 타입 추론을 한 것을 볼 수 있다. 위에서 void에 generic을 써주면 리턴 타입도 자유롭게 쓸 수 있다.

그러나 any랑 다른게 뭘까??

type overloaded = {
    (arr: any[]) : any,
}

const addNums:overloaded = (arr) => {
   arr.map(el => console.log(el));
}

addNums([1,2,3,4]);
addNums([true, false, true, false]);
addNums(["1","2","3","4"]);
addNums([false,"2",1,"4"]); 

위와 같이 해줘도 제네릭을 썼을 때처럼 에러는 나지 않는다. 하지만?

type overloaded = {
    (arr: any[]): any,
}

const addNums:overloaded = (arr) => {
   return arr[0]
}

addNums([1,2,3,4]);
addNums([true, false, true, false]);
addNums(["1","2","3","4"]);
const result = addNums([false,"2",1,"4"]); 

result.toUpperCase();
// runtime에서 에러가 난다.

위와 같이 해주면 런타임 과정에서 에러가 난다. 결론적으로 any를 쓰면 타입 체킹 과정이 생략되는 것이기 때문에 런타임으로 가기전에 에러를 찾아낼 수 없다.. 그래서 any를 쓰지 않는다. 추가적으로 제네릭은 두개 이상 쓸 수 있다.

제네릭은 call signatures를 오버로딩 하지 않아도 되도록 만들어준다(즉, 여러개의 call signature를 만들지 않아도 되도록 만들어준다).

제네릭 특징

  • 타입스크립트가 타입추론을 하도록 냅두는 것이 베스트이지만, 임의로 제네릭의 타입을 설정해줄 수 있다.
  • 제네릭 명은 자유지만 대문자로 시작하도록 한다.
  • call signature 이외에도 폭넓게 사용할 수 있다.

제네릭에 대해서 다음 포스팅에서 심층적으로 공부해보자. 매우 중요한 개념이고 다양하게 쓰인다.

profile
완벽함 보다는 최선의 결과를 위해 끊임없이 노력하는 개발자

0개의 댓글