Languages/TypeScript

[노마드코더] #3 Functions

성중 2022. 5. 15. 17:03

Call Signatures

type Add = {
    (a: number, b: number): number;
}
// type Add = (a: number, b: number) => number;

const add: Add = (a, b) => a + b

Call(=Function) Signature란 함수의 매개변수와 반환 값의 타입을 모두 type으로 미리 선언하는 것이다

* React에서 함수로 props를 보낼 때, 어떻게 작동할지 미리 설계 가능!

 

Overloading

Function(=Method) Overloading은 직접 작성하기보다 외부 패키지/라이브러리에 자주 보이는 형태로, 하나의 함수가 복수의 Call Signature를 가질 때 발생한다

type Add = {
    (a: number, b: number): number,
    (a: number, b: string): number
}

const add: Add = (a, b) => {
    if (typeof b === "string") return a;
    return a + b;
}

매개변수의 데이터 타입이 다른 경우 예외 처리

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

const add2: Add2 = (a, b, c?: number) => {
    if (c) return a + b + c;
    return a + b;
}

매개변수의 수가 다른 경우 예외 처리

 

위와 같은 함수는 거의 없지만 패키지/라이브러리에서 활용될 수 있다

router.push("/home");

router.push({
    path: "/home",
    state: 1
});

예를 들어, Next.js의 라우터 push가 대충 두 가지 방법으로 페이지를 이동한다고 할 때,

 

type Config = {
    path: string,
    state: number
}

type Push = {
    (config: Config): void,
    (config: string): void
}

const push: Push = (config) => {
    if (typeof config === "string") console.log(config);
    else console.log(config.path);
}

패키지나 라이브러리는 위와 같이 두 가지 경우의 Overloading으로 디자인되어 있을 것이다

 

Generics & Polymorphism

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

const superPrint: SuperPrint = (arr) => {
    arr.forEach(e => console.log(e));
}

superPrint([1, 2, 3]);
superPrint(["a", "b", "c"]);
superPrint([true, false, true]);
superPrint([1, "b", true]);

위와 같이 다양한 경우를 커버하는 함수를 작성할 때, 모든 조합의 Call Signature를 concrete type으로 적어주는 일은 번거롭다

 

type SuperPrint = {
    <T>(arr: T[]): T
};
// type SuperPrint = <T>(arr: T) => T;

const superPrint: SuperPrint = (arr) => arr[0]

// <number>(arr: number[]) => number
superPrint([1, 2, 3]);
// <string>(arr: string[]) => string
superPrint(["a", "b", "c"]);
// <boolean>(arr: boolean[]) => boolean
superPrint([true, false, true]);
// <string | number | boolean>(arr: (string | number | boolean)[]) => string | number | boolean
superPrint([1, "b", true]);

이 때, type에 Generic을 할당하면 호출된 값으로 concrete type을 가지는 Call Signature를 역으로 보여주는 다형성(Polymorphism)을 가진다

 

function superPrint<T>(a: T[]): T{
    return a[0];
}

물론 함수에 바로 작성해줘도 무방하다

 

그렇다면 그냥 any를 넣는 것과 Generic의 차이는 무엇일까?

type SuperPrint = {
    (arr: any[]): any
};

const superPrint: SuperPrint = (arr) => arr[0]

let a = superPrint([1, "b", true]);
// pass
a.toUpperCase();

any를 사용하면 위와 같은 경우에도 에러가 발생하지 않는다

 

type SuperPrint = {
    <T>(arr: T[]): T
};

const superPrint: SuperPrint = (arr) => arr[0]

let a = superPrint([1, "b", true]);
// error
a.toUpperCase();

Generic의 경우 에러가 발생해 보호받을 수 있다

* Call Signature를 concrete type으로 하나씩 추가하는 구조

 

type SuperPrint = {
    <T, M>(arr: T[], x: M): T
};

const superPrint: SuperPrint = (arr, x) => arr[0]

let a = superPrint([1, "b", true], "hi");

위와 같이 복수의 Generic을 선언해 사용할 수도 있다

 

type Player<T> = {
    name: string,
    extraInfo: T
};

type MePlayer = Player<MeExtra>;

type MeExtra = {age: number};

const player: MePlayer = {
    name: "joseph",
    extraInfo: {
        age: 23
    }
};

const player2: Player<null> = {
    name: "Yee",
    extraInfo: null
};

Generic은 위와 같이 원하는 만큼 커스텀 및 재사용이 가능하다

 

아마 직접 작성하기보다 패키지/라이브러리의 Generic을 활용하는 경우가 더 많을 것이다

const numArr: Array<number> = [1, 2, 3, 4];

const [state, setState] = useState<number>();

함수 뿐만 아니라 다양한 경우의 Generic을 활용할 수 있는데, 예를 들어 Array 기본 형태나 React의 useState가 Generic으로 디자인되어 있다

 

본 내용은 노마드코더의 'Typescript로 블록체인 만들기'를 바탕으로 작성되었습니다.