ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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

    댓글