-
[Typescript] typescript의 유용성, 기본적인 type 지정 방법Language/Typescript 2023. 1. 1. 18:20
1. Why use typescript
타입안정성이 좋아진다!
어떻게? 타입안정성이 주는 장점은 무엇인가?
- 코드를 실행하기 전에 런타임 에러가 날 수 있다는 것을 알 수 있다. 즉 모든 코드를 실행해보지 않고서는 그 코드가 오류가 없을 것이라는 보장이 없다. unit test 등등 여러 테스트를 거치긴하겠지만, 다른 오류도 아니고 타입에러 때문에 발생하는 오류를 잡기 위해 테스트에 리소스를 쏟는 것 보다 타입스크립트를 써서 코드 작성부터 타입에러 방지를 하는 것이 효율적일 것이다.
- 코드가 개발자의 의도와 다르게 작동하지 않도록 제어할 수 있다. 즉, 개발자에게 코드에 대한 제어권, control능력을 주게 된다. 작성하는 코드에 타입을 지정해준다는 것은 어떠한 의도를 가지고 코드 문서를 작성하는 세부 규칙을 만들었다는 뜻이고 그것을 만든 것이 바로 개발자이기 때문이다.
- 개발자의 실수를 조기에 발견할 수 있다. 개발자도 사람이고, 별도의 규칙 문서를 만들고 작업을 진행하는 것이 아니기 때문에, 실수할 수 있다. 예를 들어 자바스크립트에서는 넘버 타입의 인자를 2개 받아야 하는 함수에 깜빡하고 인자를 1개 주거나, 스트링 인자를 주더라도 코드를 실행하기 전까지는 코드 오류가 발생할 것을 알 수 없었는데 타입스크립트는 그것을 코드를 실행하기 전에 알 수 있게 해준다.
- 타입을 디자인하면서 프로그래밍을 할 수 있기 때문에 세밀한 설계가 가능할 것 같다(내생각!?)
2. Overview of Typescript
2.1 Implicit Types vs Explicit Types
- 명시적 타입 : 타입을 명시하면 타입체커가 명시된 타입과 맞는지 확인해준다.
- 타입추론 : 타입을 명시하지 않으면 타입체커가 타입을 추론한다.
// 타입추론 let a = "hello" // type : string a = "a" // OK a = 1 // TypeError let c = [1,2,3,4] c.push("1") // TypeError // 명시적 타입 let b : boolean = true b = "true" // TypeError let d : number[] = [] // 타입 추론을 사용할 수 없는 경우는 타입을 명시해주어야 한다. d.push("1") // TypeError
2.2 Types Of TS
✅ 배열: 자료형[]
✅ 숫자: number
✅ 문자열: string
✅ 논리: boolean
✅ optional parameter(선택적 변수) ⇒ or undefined
const player : { name: string, age?: number //optional ==> number || undefined } = { name: "nico" } if(player.age < 10) {} // Error : Object is possibly 'undefined' if(player.age && player.age < 10) {} // player.age가 undefined일 가능성 체크
// why??? const player : object = { name : "nico" } // TypeError : property 'name' does not exist on type 'object'
✅ Alias(별칭) 타입 : 반복되는 타입을 “변수”처럼 저장해서 재사용할 수 있다.
type Player = { name: string, age?:number } const player : Player = { name: "nico" } // In function, how about set return type? function playerMaker1(name:string) **: Player** { //:Player가 return의 타입 return { name //shortcut of name:name } } // In arrow function, how to set return type const playerMaker2 = (name:string) **: Player** => ({name}) //playerMaker1 === playerMaker2 const nico = playerMaker1("nico") nico.age = 12
✅ readonly : readonly가 있으면 최초 선언 후 수정 불가 ⇒ immutability(불변성) 부여
// In Object type Player = { readonly name:string age?:number } const playerMaker = (name: string): Player => ({name}) const nico = playerMaker("nico") nico.name = "aa" // error // In Array const numbers: readonly number[] = [1, 2, 3, 4] numbers.push(1) // error const names : readonly string[] = ["1" , "2,"] names.push("a") // error
✅ Tuple : 정해진 요소의 개수와 순서에 따라 배열 선언 ⇒ 때때로 API에서 데이터를 받을 때, 튜플 타입의 데이터를 받을 때가 있음. 그 때 쓴다.
const player: [string, number, boolean] = [] // Error : Type '[]' is not assignable to type '[string, number, boolean]'. Source has 0 elements but target requires 3. const player: [string, number, boolean] = ["nico", 1, true] player[0] = 2 // error // combine readonly const player: readonly [string, number, boolean] = ["nico", 1, true] player[0] = "nice" // error
✅ unknown : 타입을 알 수 없을 때 쓴다.
let a : unknown let b = a + 1 // a가 unknown이기 때문에 error **// So use typeof!!** if (typeof a === 'number') { let b = a + 1 } if (typeof a === 'string') { let b = a.toUpperCase() }
✅ void : 아무것도 return하지 않는 함수에서 반환 자료형
function hello() { console.log('x') } const a = hello() a.toUpperCase() // error : property toUpperCase does not exist on type "void"
✅ never : 발생할 수 없는 타입. 항상 오류를 발생시키거나 절대로 값을 반환(return)하지 않는 타입.
function hello():never { 🚫return "a" } // error function hello():never { throw new Error("zzz") } // OK // 함수의 return type이 2개 이상일 때 function temp(name:string|number):never { name + 1 } // error name : string | number function temp(name:string|number) { if (typeof name === "string") { name // name:string } else if (typeof name === "number") { name // name:number } else { name // name : never --> this code never ever run. 절대 실행되어서는 안 됨. 타입이 제대로 들어왔다면 위 두 결과 중 하나가 반환 됨. // => never means that the end of the function will **never** be reached. (never는 함수의 끝에 절대 도달할 수 없음을 뜻한다.) } }
3. Functions
3.0 Call Signatures
- Call(=Function) Signature란 함수의 매개변수와 반환 값의 타입을 모두 type으로 미리 선언하는 것이다.함수 위에 마우스를 올렸을 때 나오는 것(인자와 리턴값의 타입)이 그 함수의 call signature 이다. Call Signature are the argument types and return type of a function.
type Add = (a: number, b: number) => number; const add: Add = (a, b) => a + b;
3.1 Overloading
- Function(=Method) Overloading은 직접 작성하기보다 외부 라이브러리에 자주 보이는 형태로, 하나의 함수가 복수의 Call Signature를 가질 때 발생한다.
- 동일한 이름에 매개 변수와 매개 변수 타입 또는 리턴 타입이 다른 여러 버전의 함수를 만드는 것을 말합니다. TypeScript에서는 오버로드 signatures을 작성하여 "다양한 방식으로 호출할 수 있는 함수"를 지정할 수 있습니다.
type Add = { (a: number, b: number): number, (a: number, b: string): number } // catchAsync 오버로딩할걸!!! // 매개변수의 데이터 타입이 다른 경우 예외 처리 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 } // (a:number, b:number, c?:number):number와 같은 의미. 즉 c가 optional type Add3 = { (a: number, b: number, c?: number): number } // Add2 === Add3 const add2: Add2 = (a, b, c?: number) => { if (c) return a + b + c; return a + b; }
위와 같은 함수는 거의 없지만 외부 라이브러리에서 활용될 수 있다
- 예를 들어, Next.js의 라우터의 push 메소드가 두 가지 인자를 받아 페이지를 이동시킨다고 할 때, 패키지나 라이브러리는 아래와 같이 두 가지 경우의 Overloading으로 디자인되어 있을 것이다
router.push("/home"); router.push({ path: "/home", state: 1 }); 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); }
3.2 Generics
?? Polymorphism(다형성)
- poly란? : many, serveral, much, multi 등과 같은 뜻
- morphos란? : form, structure 등과 같은 뜻
- polymorphos = poly + morphos = 여러 다른 구조
- concrete type: number, boolean, void 등 지금까지 배운 타입
- generic type : 타입의 placeholder
type SuperPrint = { (arr: number[]): void (arr: boolean[]): void (arr: string[]): void } const superPrint : SuperPrint = arr => { arr.forEach(i => console.log(i) } superPrint([1,2,3,4]) //OK superPrint([true,true,true,true]) //OK superPrint(['a','b','c','d']) //OK but, superprint를 저렇게 쓰는 것은 불편. superPrint([1,2,true,'d']) // how about this? // Error : // No overload matches this call. // Overload 1 of 3, '(arr: number[]): void', gave the following error. // Type 'boolean' is not assignable to type 'number'. // Overload 1 of 3, '(arr: number[]): void', gave the following error. // Type 'string' is not assignable to type 'number'.(2769) // 즉, concrete type으로 type을 오버로딩하면 모든 가능성을 조합해야 하는 불편함이 있다. 또한 call signature를 미리 알 수 없는 때도 분명 있다. **// so, Generic type을 사용한다!! // <TypePlaceholder> 이게 generic을 받는 다는 의미!!** type SuperReturn = { <T> (arr: T[]): T } const superReturn: SuperReturn = arr => arr[0] const a = superReturn([1,2,3,4]) const b = superReturn([true,true,true,true]) const c = superReturn(['a','b','c','d']) const d = superReturn([1,2,true,'d']) console.log(a) // 1 console.log(b) // true console.log(c) // "a" console.log(d) // 1
- 그렇다면 그냥 any를 넣는 것과 Generic의 차이는 무엇일까?
type SuperPrint = { (arr: any[]): any } const superPrint: SuperPrint = (arr) => arr[0] let a = superPrint([1, "b", true]); a.toUpperCase(); // pass
any를 사용하면 위와 같은 경우에도 에러가 발생하지 않는다.
type SuperPrint = { (arr: T[]): T } const superPrint: SuperPrint = (arr) => arr[0] let a = superPrint([1, "b", true]); a.toUpperCase(); // error
Generic의 경우 에러가 발생해 보호받을 수 있다
- Call Signature를 concrete type으로 하나씩 추가하는 형태이기 때문!
type SuperPrint1 = { <T,M> (arr: T[], x: M)**: T** } type SuperPrint2 = <T,M> (arr: T[], x: M) **=> T** // SuperPrint1 === SuperPrint2 그런데 뒤에 함수 리턴 타입을 지정하는 방식을 헷갈리면 안된다! const superPrint: SuperPrint1 = (arr, x) => arr[0] let a = superPrint1([1, "b", true], "hi"); console.log(a) // 1 function superPrint<T>(a:T[]) { return a[0] }
위와 같이 복수의 Generic을 선언해 사용할 수 있다
3.4 Conclusions
- 제네릭은 선언 시점이 아니라 생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법이다.
- 제네릭을 사용해 타입을 생성할 수도 있지만, 어떤 경우에는 타입을 확장할 수도 있다.
type Player<T> = { name: string, extraInfo: T }; const me1 : Player<{favFood:string}> = { name:"me", extraInfo : { favFood : "kimchi" } } // 이렇게 작성하기보다, type MePlayer1 = Player<{favFood:string}> const me2 : MePlayer1 = { name:"me", extraInfo : { favFood : "kimchi" } } // 이게 낫다. 또는, type MeExtra = { favFood:string } type MePlayer2 = Player<MeExtra> const me2 : MePlayer2 = { name:"me", extraInfo : { favFood : "kimchi" } } // 이게 더 낫다 const player: MePlayer = { name: "joseph", extraInfo: { age: 23 } } // 즉 type끼리 재사용이 가능하다!
Generic은 위와 같이 원하는 만큼 커스텀 및 재사용이 가능하다
아마 직접 작성하기보다 패키지/라이브러리의 Generic을 활용하는 경우가 더 많을 것이다
const numArr: Array = [1, 2, 3, 4]; const [state, setState] = useState<number>();
함수 뿐만 아니라 다양한 경우의 Generic을 활용할 수 있는데, 예를 들어 Array 기본 형태나 React의 useState가 Generic으로 디자인되어 있다
'Language > Typescript' 카테고리의 다른 글
[Typescript] Generic (0) 2023.01.11 [Typescript] Class (0) 2023.01.11 [Typescript] undefined 관련 error 처리 방법 (0) 2023.01.06 Typescript 기본 세팅 (1) 2022.12.26