#3 Functions

Haizel·2023년 4월 17일
0

🆃 TypeScript

목록 보기
2/4
post-thumbnail

Call Signatures

  • 함수 위에 마우스를 올려놓았을 때 볼 수 있는데, 함수의 인자(arguments)타입과 & 반환타입을 알려준다.
  • ex) const add: (a: number, b: number) ⇒ number

나만의 Call Signatures 선언하기

//1. 함수의 인자타입/반환타입을 정한 함수 타입을 만든다.
type Add = (a: number, b: number) => number;

//2. 만든 함수 타입을 타입으로 선언하고, 코드를 구현한다.
const add:Add = (a, b) => a + b;

오버로딩(overloading)

  • 오버로딩은 함수가 서로 다른 여러 개의 Call Signatures를 가지고 있을 때 발생시키는 함수를 말한다.

Case 1 ) 다른 Call signatures & 같은 파라미터 개수

  • 나쁜 예시 but 설명을 도울 수 있는 코드이다.
/**/**1. 서로 다른 call signatures를 가지고 있는 함수 타입을 만든다.
type Add = {
 //다른 인자 타입, 2개의 파라미터
 (a: number, b: number) : number
 (a: number, b: string) : number
}

//2. 만든 함수 타입을 타입으로 선언하고, 코드를 구현한다.
const add:Add = (a, b) => {
	if(typeof b === "string") reuturn a;
  //아니라면
	return a + b; 
}

여기서 잠깐 벗어나 Next.JS(React.JS의 프레임워크)로 예시를 들어보자.

//Next.JS는 라우터를 가지는데 -> 2가지 방법으로 페이지를 이동할 수 있다.

//1. string으로 
.push("/home")

//2. object로 
Router.push({
	path "/home",
	state : true
})

//--> 이것이 바로 완벽한 오버로딩의 예시이다.

→ 위 Next.JS 의 코드를 TypeScript로 바꿔보자.

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

type Push = {
	(path :string) :void //! void는 아무것도 리턴하지 않는다.
	(config :config) :void //여기서 config는 Config 타입의 객체이다.
}

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

다시 TypeScript의 오버로딩(overloading)로 돌아와서

  • 오버로딩의 또 다른 특징은 다른 Call signatures이 다른 여러 개의 argument를 가지고 있을 때 발생하는 효과가 있다는 점이다.

Case 2 ) 다른 Call signatures & 다른 파라미터 개수

/**/**1. 서로 다른 call signatures, 인자(파라미터) 개수를 가지고 있는 함수 타입을 만든다.
type Add = {
 //파라미터 2 vs 3
 (a: number, b: number) : number
 (a: number, b: number, c: number) : number //--> 즉 여기서 c는 있을 수도, 없을 수도 있는 옵션이다.
}

//2. 만든 함수 타입을 타입으로 선언하고, 코드를 구현한다.
const add:Add = (a, b, c?.number) => {
//c는 옵션사항으로 c의 타입은 number일 것이라고 타입을 정해준다.
if(c) return a + b + c
	return a + b
}

add(1, 2)
add(1, 2, 3) //두 호출 모두 정상적으로 작동한다.

→ 다시 말하지만 해당 예시는 이해를 돕기 위한 코드일 뿐, 실제 구현에선 많이 사용하진 않는다. 오히려 위 config 예시가 실제 사용에선 많이 쓰인다.


다형성(polymorphism)


❓poly란?

= many, several, much, multi 등

❓morphos란?

= form, structure

❓polymorphos

= poly + morphos = 여러 다른 구조

❗️polymorphism(다형성)

= 여러타입을 받아드려 → 여려 형태를 가지는 것을 의미해,

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


  • 타입스크립트가 다형성을 형성하는 방법 중 제네릭에 대해 배워보자.
  • 그 전에 concrete typegeneric(제네릭) 타입의 정의를 살펴보자

💡 concrete type

  • number 타입, string 타입, boolean 타입, void 타입, unkown 타입 등 기본 타입을 말한다.

💡 generic(제네릭)

사전적 정의

C#, Java 등의 언어에서 재사용성이 높은 컴포넌트를 만들 때 자주 활용되는 특징으로, 특히 여러가지 타입에서 동작하는 컴포넌트를 생성하는데 사용된다.

타입스크립트에서

❗️ 역할 :
타입의 placeholder 같은 역할로 타입을 마치 함수의 파라미터처럼 사용하는 것을 의미하며,
타입스크립트 스스로 타입을 유추하도록 한다.
❗️ 장점 :
1. 즉 타입스크립트에서 제네릭을 사용함으로써 구체적인 타입을 지정하지 않고 다양한 인수와 리턴 값에 대한 타입을 처리할 수 있다.
2. 또한 제네릭을 통해 인터페이스, 함수 등의 재사용성을 높일 수 있다.

  • placeholder : 어떠한 것을 대신한다는 뜻이다.

Q. 같은 코드를 기본 타입 vs generic 타입으로 구현한다면?

  • 배열을 인자로 받아, 배열의 각 인자를 콘롤로 찍는 함수를 구현해라. (단, 인자의 타입은 알 수 없다)

A1) 기본 타입 ( ⭐️ 여기서 number[], boolean[], string[]은 → concrete type 이 아니다)

type SuperPrint = {
  //superPrint 함수의 파라미터 타입을 알 수 없으니 모든 타입에 대해 타입 지정을 해줘야 한다.
  //해당 함수는 콘솔로 찍을 뿐 아무것도 리턴하지 않으니 리턴 타입은 void가 된다.
    (arr: number[]): void; // -> number배열
    (arr: boolean[]): void; // -> boolean배열
    (arr: string[]): void; //..
		(arr: (number|boolean)[]) : void //..
    //.. 이 외에도 모든 타입에 대해 타입 지정을 해줘야해서 아주아주 귀찮다.
};

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

superPrint([1, 2]) //1, 2
superPrint([true, false]); //true, false

지정해준 타입 외에도 모든 타입에 대해 타입 자정을 해줘야하므로, 아주아주 귀찮고 번거롭다.


A2) generic : <>

  • call Signature를 작성할 때, 인자의 타입을 확실히 모를때 → generic 를 사용한다.
type SuperPrint = {
/* <> 안에 제네릭 이름을 넣어 사용한다 
   -> 이때 이름은 마음대로 정할 수 있으나 대문자로 시작해야 하며, 통상 T, V를 많이 쓴다. */
	<T>(arr : T[]): T //--> T는 배열에서 오고, 함수의 첫번째 파라미터에서 오는거라고 타입스크립트에게 알려줌.
};

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

/* 타입 스크립트는 함수의 인자값을 보고 타입을 유추해 -> call signature을 알려준다. */

const a = superPrint([1, 2]) //--> const a: number
const b = superPrint([true, false]) //--> const b: boolean
const c = uperPrint(['hi', 'hello']) //--> const c: string
const d = superPrint([true, 1, 'hi']) //--> const d: string|number|boolean

제네릭 추가 참고자료

Generics Recap


💡 generic vs any 차이점

언뜻 보면 generic 은 아무타입이나 받을 수 있는 any와 같아 보이는데,
→ 이처럼 둘의 공통점은 어떤 타입이든 받을 수 있다는 점이다.
하지만 완벽히 같다면 굳이 generic 를 쓰지 않아도 될거 같은데, 둘의 차이점은 무엇일까.

💡 generic 은 해당 타입 정보를 알 수 있다.

  • any는 타입 정보를 :any로 밖에 알 수 없지만,
  • generic은 해당 타입 정보를 정확히 할 수 있다.

따라서 제네릭 대신 any를 사용하게 되면

...
const d = superPrint([true, 1, 'hi']) // d = true
d.toUpperCase() //-->  실행은 되나, 에러가 발생한다.

//하지만 제네릭을 사용하게 되면,
d.toUpperCase() //--> 코드에 빨간 밑줄이 그어지고, 코드 자체가 실행되지 않는다.

🔥 최종 정리
1. genericany
는 어떤 타입이든 받을 수 있다는 공통점이 있지만,
2. any는 받았던 인수들의 타입을 활용하지 못하는 반면,
3. generic 은 해당 타입의 정보를 잃지 않고 다른 코드에 활용할 수 있다.


❓ 만약 Call signature에 제네릭을 한 개 이상 사용하고 싶다면?

type SuperPrint = {
//--> 함수의 첫번째 인자로는 배열타입, 두번째 인자로는 M타입이 들어온다.
	<T, V>(a : T[], b:V): T
};

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

//--> 타입스크립트는 제네릭의 순서를 기반으로 제네릭의 타입을 인식한다.
const a = superPrint([1, 2], "x") // <number, string>(a: number[], b:string)
const b = superPrint([true, false], 1)// <boolean, number>(a: boolean[], b:number)
const c = uperPrint(['hi', 'hello'], false) // <string, boolean>(a:string[], b:boolean)
const d = superPrint([true, 1, 'hi'], []) // <string|number|boolean, never[]>(a: string|number|boolean[], b:never[])

제네릭 은 선언 시점이 아니라
생성 시점에 타입을 명시하여 하나의 타입만이 아닌, 다양한 타입을 사용할 수 있도록 하는 기법이다.

제네릭을 좀 더 간단하게 쓸 수 있는 방법

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

제네릭 응용 : 코드 확장 및 재사용


기본코드

type Player<T> = {
	name : string,
	extraInfo : T //extraInfo는 말그래도 number, string, boolean 등 모든 타입을 받을 수 있다. 단 any를 사용하면 보호받지 못하므로 -> 제네릭을 사용한다.
}

const nico: Player<{favFood : string}> = {
	name : "nico",
	extraInfo : {
			favFood : "kimchi"
  }
}
  1. 기본코드 확장
type Player<T> = {
  name: string;
  extraInfo: T;
};

type NivoExtra = {
  favFood: string;
};

type NicoPlayer = Player<NivoExtra>;

const nico: NicoPlayer = {
  name: "nico",
  extraInfo: {
    favFood: "kimchi",
  },
};
  1. 기본코드 재사용
type Player<T> = {
  name: string;
  extraInfo: T;
};

...

//lynn은 extraInfo가 없는 경우
const lynn: NicoPlayer<null> = {
  name: "lynn",
  extraInfo: null
};

이처럼 많은 것들이 들어 있는 큰 타입을 가지고 있고(type Player), 그 중 하나가 달라질 수 있는 타입(extraInfo)이라면
→ 거기에 제네릭을 넣어주면 재사용이 가능하다.

  1. 제네릭을 사용하는 또 다른 방법s
//1.  number타입으로 이뤄진 배열을 만들 경우
type A = number[]
// ⬇️ 제네릭을 쓸 수 있다.
type A = Array<number>
let a: A = [1, 2, 3, 4]

//2. number타입으로 이뤄진 배열을 함수 인자로 받을 경우
function printAllnumbers(arr : number[]) {..}
// ⬇️ 제네릭을 쓸 수 있다.
function printAllnumbers(arr : Array<number>) {..}
  1. React.js(useState) + TypeScript
//--> number타입의 useState가 된다.
const ... = useState<number>()

참고자료 | Nomad: TypeScript

profile
한입 크기로 베어먹는 개발지식 🍰

0개의 댓글