프론트엔드/TypeScript

[TypeScript] call signatures, 오버로딩, 다형성, 제네릭

s_omi 2024. 1. 20. 14:29
728x90
반응형
SMALL

호출 시그니처(Call Signatures)

프로퍼티로 호출 가능한 것을 설명하려면 객체 타입에 Call Signature을 작성할 수 있다.

Call Signatures는 다음과 같이 함수의 매개 변수(parameter)와 반환 타입을 지정하여 이 함수가 어떻게 호출되는지 설명해주는 부분을 말한다.

const add = (a : number, b : number) => a + b;
// 위의 코드에서 a와 b의 타입 지정을 하고 싶지 않다면 call signature 타입을 만들어 사용

// call signature 타입 사용 후 
type Add = (a : number, b : number) => number; // call signature
const add : Add = (a, b) => a + b;

 

 

오버로딩(Overloading)

우리가 실제로 구현하게 되면 패키지나 라이브러리들을 자주 사용하게 될 텐데 이때 패키지나 라이브러리들은 오버로딩을 엄청 많이 사용한다.

오버로딩은 함수가 서로 다른 형태의 여러 개 call signatures를 가지고 있을 때 발생시킨다.

// 매개변수의 데이터 타입이 다른 경우 예외 처리
type Add = {
	(a: number, b: number) : number,
	(a: number, b: string) : number,
}

const add : Add = (a, b) => a + b;  // 에러, 이때 b는 string | number 타입인데 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);
}

 

 

다형성(Polymorphism)

poly는 many, several, much와 같은 뜻이고 morphism은 form, structure와 같은 뜻이다. 즉 다형성이란 여러가지 다른 모양(형태)를 뜻한다.

인자들과 반환값에 대하여 형태(타입)에 따라 그에 상응하는 형태(타입)를 갖을 수 있다.

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

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

const a = superPrint([1, 2, 3]); 
const b = superPrint([true, false, true]);
const c = superPrint(["a", "b"]);
const d = superPrint([1, 2, "a", "b"]);   // 에러, number | string 타입이 명시되어 있지 않기 때문

// 때문에 위와 같이 일일이 주는 대신 generic(타입의 placeholder와 같은 것)을 사용

 

 

제네릭(Generic) 타입

  • 사용하는 이유
    • 우리가 call signature을 작성할 때 여기 들어올 확실한 타입을 모를 때 generic을 사용한다.
    • C#이나 Java와 같은 언어에서 재사용 가능한 컴포넌트를 만들기 위해 사용하는 기법이다.
    • 단일 타입이 아닌 다양한 타입에서 작동할 수 있는 컴포넌트를 생성할 수 있다.
    • 구체적인 타입을 지정하지 않고 다양한 인수와 리턴 값에 대한 타입을 처리할 수 있다.
    • 타입스크립트에서 제네릭을 통해 인터페이스, 함수 등의 재사용성을 높일 수 있다.
  • any와의 차이점
    • 해당 타입에 대한 정보를 잃지 않는다.
    • any는 any로서 밖에 알 수 없지만 generics는 타입 정보를 알 수 있다.
// type SuperPrint = <T>(arr : T[]) => T  // generic 사용, Typescript가 타입을 유추

// const superPrint : SuperPrint = (arr) => {
// 	return arr[0];
// }

// 위의 방법보다 더 자주 쓰일 generic 사용한 코드
function superPrint<T>(arr : T[]){
	return arr[0];
}

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

a.toUpperCase();
// any를 사용하면 위와 같은 경우에도 에러가 발생하지 않는다.
// generic을 사용하면 에러가 발생해 보호받을 수 있다.
type Player<E> = {
	name : string,
	info: E
}
type Age = { 
	age : number 
}
type firstPlayer = Player<Age>

const person : firstPlayer = {
	name: "hi",
	info: { age: 12 }
}

const person2 : Player<null> = {
	name: "hi2",
	info: null
}

 

아래는 실제로 있는 generic 사용 예시이다.

type arr = Array<number>
let a : arr = [1, 2, 3, 4];
728x90
반응형
LIST