const add: (a: number, b: number) ⇒ number
//1. 함수의 인자타입/반환타입을 정한 함수 타입을 만든다.
type Add = (a: number, b: number) => number;
//2. 만든 함수 타입을 타입으로 선언하고, 코드를 구현한다.
const add:Add = (a, b) => a + b;
나쁜 예시
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) } }
/**/**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 예시가 실제 사용에선 많이 쓰인다.
❓poly란?
= many, several, much, multi 등
❓morphos란?
= form, structure
❓polymorphos
= poly + morphos = 여러 다른 구조
❗️polymorphism(다형성)
= 여러타입을 받아드려 → 여려 형태를 가지는 것을 의미해,
= 인자들과 반환값에 대하여 형태(타입)에 따라 그에 상응하는 형태(타입)을 갖을 수 있다.
concrete type
과 generic(제네릭)
타입의 정의를 살펴보자💡 concrete type
- number 타입, string 타입, boolean 타입, void 타입, unkown 타입 등 기본 타입을 말한다.
💡 generic(제네릭)
사전적 정의
C#, Java 등의 언어에서 재사용성이 높은 컴포넌트를 만들 때 자주 활용되는 특징으로, 특히 여러가지 타입에서 동작하는 컴포넌트를 생성하는데 사용된다.
타입스크립트에서
❗️ 역할 :
타입의 placeholder 같은 역할로 타입을 마치 함수의 파라미터처럼 사용하는 것을 의미하며,
타입스크립트 스스로 타입을 유추하도록 한다.
❗️ 장점 :
1. 즉 타입스크립트에서 제네릭을 사용함으로써 구체적인 타입을 지정하지 않고 다양한 인수와 리턴 값에 대한 타입을 처리할 수 있다.
2. 또한 제네릭을 통해 인터페이스, 함수 등의 재사용성을 높일 수 있다.
- placeholder : 어떠한 것을 대신한다는 뜻이다.
기본 타입
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
: <>
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
💡
generic
vsany
차이점언뜻 보면
generic
은 아무타입이나 받을 수 있는any
와 같아 보이는데,
→ 이처럼 둘의 공통점은 어떤 타입이든 받을 수 있다는 점이다.
하지만 완벽히 같다면 굳이generic
를 쓰지 않아도 될거 같은데, 둘의 차이점은 무엇일까.
💡
generic
은 해당 타입 정보를 알 수 있다.
- any는 타입 정보를 :any로 밖에 알 수 없지만,
- generic은 해당 타입 정보를 정확히 할 수 있다.
따라서 제네릭 대신 any를 사용하게 되면
...
const d = superPrint([true, 1, 'hi']) // d = true
d.toUpperCase() //--> 실행은 되나, 에러가 발생한다.
//하지만 제네릭을 사용하게 되면,
d.toUpperCase() //--> 코드에 빨간 밑줄이 그어지고, 코드 자체가 실행되지 않는다.
🔥 최종 정리
1.generic
과any
는 어떤 타입이든 받을 수 있다는 공통점이 있지만,
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"
}
}
type Player<T> = {
name: string;
extraInfo: T;
};
type NivoExtra = {
favFood: string;
};
type NicoPlayer = Player<NivoExtra>;
const nico: NicoPlayer = {
name: "nico",
extraInfo: {
favFood: "kimchi",
},
};
type Player<T> = {
name: string;
extraInfo: T;
};
...
//lynn은 extraInfo가 없는 경우
const lynn: NicoPlayer<null> = {
name: "lynn",
extraInfo: null
};
이처럼 많은 것들이 들어 있는 큰 타입을 가지고 있고(
type Player
), 그 중 하나가 달라질 수 있는 타입(extraInfo
)이라면
→ 거기에 제네릭을 넣어주면 재사용이 가능하다.
//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>) {..}
//--> number타입의 useState가 된다.
const ... = useState<number>()