게으른개발너D
Functions - call signature, overriding, polymorphism, generics 본문
Functions - call signature, overriding, polymorphism, generics
lazyhysong 2023. 4. 4. 00:24
✨ Call Signatures ✨
arrow function 으로 typescript 함수를 작성해 보자
const add = (a: number, b: number) => a + b;
우리는 위에서처럼 타입을 적지않고 add 함수만의 타입을 만들고 싶다.
type Add = (a: number, b: number) => number;
const add:Add = (a, b) => a + b;
이게 바로 함수의 call signature 타입을 만드는 것이다.
이렇게 우리는 함수를 구현하기 전에 타입을 미리 정할 수 있다.
처음에 우리가 타입을 생각할 수 있도록 할 수 있다.
✨ Overloading ✨
우리는 대부분 다른 사람들이 만든 외부 라이브러리를 사용할텐데, 이런 패키지나 라이브러리들은 오버로딩을 엄청 많이 사용한다.
이전 게시글에 type signature을 다뤘었는데, 우리가 타입스크립트에게 이 함수가 어떻게 호출되는지 설명해주는 부분이었다.
type Add = (a: number, b: number) => number
이 방법은 signature을 만드는 가장 짧은 방법이다.
아래 이러한 방법도 가능하다.
type Add = {
(a: number, b: number): number
}
이러한 방법이 존재하는 이유는 오버로딩 때문이다.
오버로딩은 함수가 여러개의 call signatures를 가지고 있을 때 발생시킨다.
그냥 여러개가 아니라 서로 다른 여러개의 call signature를 가졌을 때이다.
type Add = {
(a: number, b: number): number
(a: number, b: string): number
}
아주 나쁜 예이지만 이런식으로 Add 타입을 정의할 수 있다.
그러면 b는 number도 될 수 있고 string도 될 수 있다. (string | number)
그렇게 된다면 아래 함수는 에러 표시가 뜨게 된다.
const add:Add = (a, b) => a + b
모두 만족할 수 있도록 하려면 함수가 이런식으로 구성되면 될 것이다.
const add: Add = (a, b) => {
if(typeof b === "string") return a;
return a + b;
}
아주 나쁜 예시지만 오버로딩의 핵심을 알아볼 수 있다.
우리가 사용하는 library에는 몇가지 함수가 있다.
그리고 그 함수에는 string을 보낼 수 있거나, configuration같은 객체를 보낼 수 있도록 허용했을 것이다.
nextjs에서의 router를 예로 들어보자
Router.push("/home");
Router.push({
path: "/home",
state: 1,
});
/home으로 보내는 기능을 string으로도, object로도 쓸 수 있다.
비슷한 예로 오버로딩이 쓰이는 코드를 작성해 보자.
type Config = {
path: string,
state: object,
}
type Path = {
(path: string): void
(config: Config): void
}
const path: Path = (config) => {
if(typeof config === "string") {
console.log(config);
} else {
console.log(config.path);
}
}
다른 오버로딩의 기능은 여러개의 argument들을 가지고 있을 때 나타나는 효과이다.
예를들어 이런 signature 타입들이 있을 수 있다.
type Add = {
(a: number, b: number): number
(a: number, b: number, c: number): number
}
다른 개수의 파라미터를 가지만 나머지 하나의 파라미터도 타입을 지정해 줘야 한다.
여기서 c 파라미터는 옵션같은 것이다. 따라서 함수는 이런식으로 적어줄 수 있다.
const add: Add = (a, b, c?: number) => {
if(c) return a + b + c;
return a + b;
};
✨ Polymorphism ✨
Polymorphism(다양성)은 '여러가지 다른 구조들'이란 뜻이다.
call signature를 만들자!
type SuperPrint = {
(arr: number[]): void
}
const usperPrint: SuperPrint = (arr) => {
arr.forEach(i => console.log(i))
}
배열 요소를 받아서 그 배열의 요소를 각각 콘솔로 찍는 작업이다.
여기서 문제가 있다.
파라미터로 배열을 받을 수 있는데, boolean으로 받을 수도 있고, object로 받을 수도 있고 뭐든 배열로 받을 수 있다.
오버로딩을 해주자
type SuperPrint = {
(arr: number[]): void
(arr: boolean[]): void
}
superPrint([1, 2, 3, 4]);
suserPrint([true, false, false, true]);
이렇게 하면 number 배열과 boolean 배열을 아규먼트로 넣어서 함수를 사용할 수 있다.
하지만 string 배열을 넣으면 에러가 발생한다.
또, [1, 2, true, false] 이러한 배열을 넣어도 에러가 발생한다.
그러면 string 배열과 number 도는 boolean 배열을 받는 call signature을 또 넣어줘야할까?
type SuperPrint = {
(arr: number[]): void
(arr: boolean[]): void
(arr: string[]): void
(arr: (number|boolean)[]): void
}
이렇게 하면 너무 비효율적이다.
그래서 그 대신, 우리는 generic을 사용할 것이다.
concrete type은 string, number, boolean, void, unknown 등등 이런 것들이다.
여기서 우리는 타입스크립트한테 generic type을 받을 거라고 알려줄 거다.
generic이란, type의 placeholder같은 것인데, concrete type을 사용하는 것 대신 쓸 수 있다.
우린 타입스크립트로 placeholder를 작성할 수 있고 타입스크립트는 그게 뭔지 추론해서 함수를 사용하는 것이다.
우리가 call signature를 작성하는데, concrete type을 알 수 없을 때도 있다. 그럴 때 generic을 사용한다.
generic을 사용하려면 먼저, 타입스크립트에 generic을 사용하고싶다고 알려줘야한다.
방법은 <> 이 괄호 안에 generic 이름을 넣어주는 것이다.
여러 패키지나 라이브러리를 쓸 텐데 가장 많이 보는 건 T 또는 V이다.
type SuperPrint = {
<TypePlaceholder>(arr: number[]): void
}
이렇게 해당 call signature에 generic을 쓰고싶다고 알렸다.
그리고 저기 number를 typePlaceholder로 바꿔줘야한다.
type SuperPrint = {
<TypePlaceholder>(arr: TypePlaceholder[]): void
}
superPrint([1, 2, 3, 4]);
superPrint([true, false, false, true]);
superPrint(["a", "b", "c"]);
superPrint([1, 2, true, false]);
이렇게 바꾸면 첫번째에선 타입스크립트가 해당 라인에서 superPrint가 number 타입의 배열로 동작한다는 걸 알게된다.
마우스를 올려보면 보일 것이다. <number>(arr: number[]) => void
타입스크립트가 타입을 유추하고 그 유추한 타입으로 call signature를 우리에게 보여주는 것이다.
함수의 리턴 타입을 배열의 첫번째 요소를 리턴하는 걸로 바꿔보자.
type SuperPrint = {
<TypePlaceholder>(arr: TypePlaceholder[]): TypePlaceholder
}
const superPrint: SuperPrint = (arr) => arr[0]
이 superPrint 함수는 많은 형태를 가지고 있다. 이게 바로 Polymorphism이다.
✨ Generics ✨
generic은 우리가 요구한 대로 signature를 생성해 줄 수 있는 도구이다.
SuperPrint type의 generic을 하나 더 생성해주자.
type SuperPrint = <T, M>(a: T[], b: M) => T
타입스크립트는 제네릭을 처음 인식했을 때와 제네릭의 순서를 기반으로 제네릭의 타입을 알게 된다.
✨ Conclusions ✨
라이브러리를 만들거나 다른 개발자가 사용할 기능을 개발하는 경우엔 제네릭이 유용할 것이다.
SuperPrint를 함수로 바로 구현할 수 있다.
function superPrint<V>(a: V[]) {
return a[0]
}
함수를 사용할 때 타입스크립트가 유추하는 것 말고 내가 직접 확실하게 정의해 줄 수도 있다.
const a = superPrint<boolean>([true, false, false, true])
제네릭을 사용해서 타입을 생성할 수도 있고 어떤 경우는 타입을 확장할 수 있다.
type Player<E> = {
name: string
extraInfo: E
}
const hwayeon: Player<{favFood: string}> = {
name: "Hwayeon",
extraInfo: {
favFood: "moosaengchae",
}
}
이렇게 바꿔줄 수도 있다.
type Player<E> = {
name: string
extraInfo: E
}
type HwayeonPlayer = Player<{favFood: string}>
const hwayeon: HwayeonPlayer = {
name: "Hwayeon",
extraInfo: {
favFood: "moosaengchae",
}
}
또는 이렇게 확장 시킬 수도 있다.
type Player<E> = {
name: string
extraInfo: E
}
type HwayeonExtra = {
favFood: string
}
type HwayeonPlayer = Player<HwayeonExtra>
const hwayeon: HwayeonPlayer = {
name: "Hwayeon",
extraInfo: {
favFood: "moosaengchae",
}
}
extraInfo가 null인 새로운 object를 생성해 보자
const lucky: Player<null> = {
name: "lucky",
extraInfo: null
}
제네릭을 많이 보게될 건데, 타입스크립트는 제네릭으로 많이 이루어져 있다.
예를들어 이런게 있다.
type A = Array<number>
let a:A = [1, 2, 3, 4]
파라미터로 number array를 받는 함수는 이렇게 두가지로 정의할 수 있다.
function printAllNumbers(arr: number[]) {
}
function printAllNumbers(arr: Array<number>) {
}
리액트에서는 이렇게 쓰였었다.ㅎㅎㅎㅎㅎ
useState<number>()
'개발 > TypeScript' 카테고리의 다른 글
window 객체에 property 추가하기 (0) | 2023.08.23 |
---|---|
Classes and Interfaces - classes, interfaces, polymorphism (0) | 2023.04.04 |
Overview of Typescript (0) | 2023.04.03 |
Software Requirement (0) | 2023.04.03 |