본문 바로가기

BOOK/Effective Typescript

[이펙티브 타입스크립트| 2장] 타입스크립트의 타입 시스템

12. 함수 표현식에 타입 적용하기

 

자바스크립트(그리고 타입스크립트)는 함수의 문장(statement)와 함수 표현식(expression)을 다르게 인식한다.

 

함수의 문장과 함수 표현식

 

타입스크립트에서는 함수 표현식으로 사용하는 것이 좋다.

const add=(a:number, b:number):number => return a+b;
const minus=(a:number, b:number):number => return a-b;

위와 같이 사용하던 것을 아래와 같이 사용할 수 있다는 이점 때문이다.

type calNum=(a:number, b:number)=> number;

const add:calNum =(a, b) => return a+b;
const minus:calNum=(a, b) => return a-b;

 

 

시그니처 ⏤ 변수의 타입을 미리 정하지 않아도 자동적으로 타입이 결정되어 있는 것 / 용어 사전 ⏤가 일치하는 함수가 있을 때도 함수 표현식에 타입을 적용할 수 있다.

 

 

fetch를 적용할 때 fetch를 체크하지 않고 바로 response.ok 를 출력하게 된다면 400번대 에러가 발생했을 때 우리가 원하는 응답 형식이 아닐 수 있다. 그래서 아래와 같은 작업들이 필요하다.

 

async function checkedFetch(input:RequestInfo, init?:RequestInit){
	const response = await fetch(input,init);
    
    if(!response.ok){
    	throw new Error('Request Failed');
    }
    
    return response
}

 

이곳에서 fetch(lib.dom.d.ts)에 관련된 타입을 지정해줄 수 있는데 이런 경우 typeof를 사용해서 아래와 같이 더 간결하게 작성이 가능하다.

 

const checkedFetch:typeof fetch=async(input,init)=>{
	const response = await fetch(input,init);
    
    if(!response.ok){
    	throw new Error('Request Failed');
    }
    
    return response
}

 

13. 타입과 인터페이스 차이점 알기

 

타입을 지정해줄 때 inteface 혹은 type 두 가지 방법을 사용할 수 있습니다.

두 개를 사용할 때의 차이점을 알고 일관성 있게 사용해야합니다. 

 

1)  인덱스 시그니처

/ interface, type 모두 사용 가능

 

TObj, IObj 로 명명하는 방식은 레거시라고 들었는데 책에 나와있는대로 작성했다.

type TObj = { [key:string] : string}
inteface IObj{
 [key:string]:string;
}

const obj={
	id : 3 // error !
}

 

위와 같이 object의 key , value 값에 대한 타입을 지정해 줄 수 있다.

 

2)  함수 타입

/ interface, type 모두 사용 가능

 

3)  제네릭

/ interface, type 모두 사용 가능

 

제네릭과 관련 된 설명은 뒤에 더 자세히 할 것이기 때문에  예제 코드만 남겨둡니다.

type TPair<T>={
 first: T;
 second: T;
 }
 
interface IPair<T>{
 first: T;
 second: T;
 }

 

4)  확장

/ interface, type 모두 사용 가능

 

interface는 type을 확장할 수 있고, type은 interface를 확장할 수 있다.

그러나 전자의 같은 경우 주의사항이 존재한다.

 

 

interface는 type의 복잡한 타입(유니온)은 확장하지 못한다.

type TString = string;
type TNumber = number;

interface IType {
  [name: string]: TString | TNumber;
}

위와 같이는 사용할 수 있으나, 인터페이스에서 아래와 같은 방법은 interface에서 표현할 수 없다.

type TType = (TString | TNumber) & { name: string };


/* TType 에 올 수 있는 3가지 타입

1 ) number
2 ) string
3 ) { name : string } */

 

그 이유는 type이 interface보다 쓰임이 많게 고도화하여 사용할 수 있기 때문이다.

또 다른 예는 배열의 타입을 선언할 때이다.

 

type TType1 = [string, string, string];
type TType2 = [string, string, number];

 

인터페이스 또한 비슷하게 선언이 할 수 있지만, concat (배열과 배열을 합쳐서 새로운 배열을 만들어내는) 과 같은 함수는 사용할 수 없게 된다. 따라서 배열과 같은 선언은 type을 이용하는 것이 좋다.

 

 

5)  보강

/ interface만 사용 가능

 

보강이라는 단어가 모호할 수 있는데 '중복 선언'에 대한 여부라고 이해하면 편하다.

 

interface IType {
  name: string;
  age: number;
}

interface IType {
  city: string;
}


/*

	IType의 타입={
    name: string;
 	age: number; 
   	city: string;
    
    }
*/

 

똑같은 IType 이라는 interface 명을 사용해도 된다. 하지만, type은 불가능하다. 

 

 

결론,

복잡한 타입이라면  - type, 

API를 사용하고 그 타입에 대한 변경이 필요하고, 보강에 대한 부분이 필요하다면 - interface

 

가장 중요한 것은 프로젝트에서 어떻게 일관된 스타일을 가져가고, 보강에 대한 기법이 필요한지 고려해야한다. 

 

 

14.  타입 연산과 제네릭 사용으로 반복 줄이기

 

코드의 중복에 대한 고민이 필요하듯, 타입에 대한 중복도 고민이 필요하다.

타입의 중복을 막는 가장 좋은 방법은 타입에 이름을 붙이는 방법이다. 

 

interface Person{
   firstname:string;
   lastname:string;
}

interface PersonWithBirthDate extends Person{
   birth:Date;
}

 

위와 같이 extends (type은 & 연산자) 를이용하여 PersonWithBirthDate라는 타입을 만들어서 반복을 제거할 수 있다.

 

 

// 전체 애플리케이션의 상태를 표현
interface State {
   userId : string;
   pageTitle : string;
   pageContent : string;
}

// 애플리케이션의 부분만 표현
interface TopNavState{
   userId : string;
   pageTitle : string;
}

위와 같은 타입 정의가 있을 때 TopNavState는 State의 부분 집합이라고 생각할 수 있다. 그렇기 때문에 반복을 줄이기 위하여 아래와 같이 사용할 수 있다. 그럼에도 여전한 반복되는 요소들이 보인다.

// 애플리케이션의 부분만 표현
interface TopNavState{
   userId : State['userId'];
   pageTitle : State['pageTitle'];
}

반복을 줄이기 위해 매핑된 타입을 사용해주면 보다 간결하게 해결할 수 있다.

 

1 ) Pick

interface TopNavState{
	[k in 'userId' | 'pageTitle'] : State[k]
}

 

매핑된 타입의 정의는 다음과 같다.

Pick은 T와 K 두 가지 타입을 받아서 결과 타입을 반환한다. 

type Pick<T,K> = {[k in K] : T[k]};


// 다음과 같이 사용 가능하다.

type TopNavState=Pick<State, 'userId'|'pageTitle'>

 

2 ) Partial - optional 일 때

더해서, keyof 속성을 사용해서 해당하는 속성이 있는지 찾을 수 있다.

 

interface Options{
   width:number;
   height:number;
   color:string;
}

type optionsUpdate = { [k in keyof Options]?: Options[k]};

매핑된 타입 [k in keyof Options] 은 순회하며 Options 에 해당하는 k 값에 해당하는 속성이 있는지 찾습니다. (? 연산자는 각 속성을 선택적으로 만듭니다. - 필수 속성이 아니라는 의미) Partial 이라는 유틸리티에 잘 정의되어 있습니다. 

 

 

3 ) ReturnType

함수나 메서드의 반환 값에 명명된 타입을 만들고 싶을 수 있다. (Redux를 사용하는 경우에 많이 쓰는 유틸 타입이다.)

 

function getUserInfo(userId:string){

	// 관련 로직들
    
    
    return{
    	userId,
        name,
        age,
        height
    };
}

// 추론된 반환 타입은 { userId:string, name:string, age:number ... }

 

만약에 해당 함수를 실행시켜서 User에 할당시켜주어야 한다면 아래와 같이 return 값을 일일히 선언해주는 것은 굉장히 번거로운 일이다. (getUserInfo의 return 값이 변경되면 UserInfo 타입 또한 수정시켜주어야한다. )

type UserInfo= { userId:string, name:string, age:number, ... }

const User:UserInfo=getUserInfo(userId);

이런 경우에 ReturnType 제네릭이 사용하기 좋다. 

 

type UserInfo = ReturnType<typeof getUserInfo>

 

 

* 더 많은 타입스크립트 유틸 타입을 알아보고 싶다면 - 타입스크립트의 기본 유틸 타입

 

 

15. 동적 데이터에 인덱스 시그니처 사용하기

자바스크립트의 장점은 객체 생성 문법이 간단하다는 것이다. 타입스크립트는 객체에 대한 타입을 '인덱스 시그니처'를 명시하여 유연하게 매핑 정보를 표현할 수 있다.

 

type Rocket = { [property: string] : string }

 

[property: string] : string 와 같은 표현을 인덱스 시그니처라고 부른다.

 

인덱스 시그니처의 의미

 

- 키의 이름 : 키의 위치를 표시하는 용도 ( 타입 체커에서는 사용하지 않는 정보 )

- 키의 타입 : string, number 또는 symbol의 조합이어야 하지만 보통 string 값을 많이 쓴다.

- 값의 타입 : 어떤 것이든 가능하다.

 

const rocket:Rocket={
    name: 'falcon 9',
    variant:'v1.0'
}

 

이렇게 했을 때의 단점이 존재한다.

 

1. 모든 키 값을 허용한다. ⏤ rocket에는 name 이 들어와야 함에도, Name 이 들어와도 문제 없이 동작한다.

2. 특정 키가 필요하지 않아도 된다. ⏤ 빈 객체도 가능해진다.

3. 키 마다 다른 value 타입을 갖기 힘들다 ⏤ 객체의 모든 value가 string 값만 들어오는 것이 아니다.

 

결론적으로 인덱스 시그니처는 부정확하므로 더 나은 방법이 필요하다.

아래와 같은 방법을 쓴다면 이런 단점을 해결할 수 있다.

type Rocket = { 
   name : string;
   variant : string;
}

 

연관 배열의경우  인덱스 시그니처를 사용하는 대신 Map 타입을 사용하는 것을 고려할 수 있다. (자세한 것은 아이템 58장에)

연관 배열
연관 배열은 배열과 다르게 중괄호로 선언을 한다. 자바스크립트에서는 '객체' 라고 부른다. 배열의 index를 통해 접근을 했지만, 연관 배열의 접근은 key 값으로 한다

 

 

대안 1 - Record

 

인덱스 시그니처에 union 타입을 사용하면 warning 문구가 발생한다.

type Rocket = { [property: 'variant' | 'name' ] : string } // 오류

 

이럴 때 Record를 사용하면 좋다. Record는 키 타입에 유연성을 제공해줄 수 있는 제너릭 타입이다.

type Vec3D=Record<'x' | 'y' | 'z', number>;

 

key 값이 x | y | z 가 올 수 있고, value 에 number가 올 수 있다는 것을 의미한다.

 

대안 2 - 매핑된 타입을 사용

 

type Vec3D = {[key in 'x'|'y'|'x']:number}

 

 

16. number 인덱스 시그니처보다는 Array, 튜플, ArrayLike 사용하기

암시적 타입 강제와 같은 부분이 자바스크립트의 단점이라고 꼽을 수 있다.

"0" == 0  // true

"0" === 0 // false (암시적 타입 강제와 관련된 문제는 === 연산자를 통해 해결 가능)

 

만약 자바스크립트에서 더 복잡한 객체(ex - 배열)를 키로 사용하려고 한다면 toString 메서드가 호출 되어 해당 키가 문자열로 변환됩니다.

특히 key 를 숫자로 할려고 한다면, 자바스크립트의 런타임은 문자열로 변환시킨다.

 

x = {}
x[[1,2,3]] = 2 

console.log(x) 
// { '1,2,3' : 2 }

 

배열 또한 인덱스들이 문자열로 변환하여 사용할 수 있다.

Object.keys(x) 를 출력하면 ['0','1','2'] 의 문자열 형태이다.

const x=[1,2,3]

console.log(x[0])
//숫자 인덱스로 접근하는 경우 - 1

console.log(x['1']) 
//문자 인덱스로 접근하는 경우 - 2

 

이런 부분에서 혼란이 생기게 되는데, 타입스크립트는 이러한 것을 바로 잡기 위해 숫자 key 값을 허용한다. (문자열 key 와는 다르다고 인식) . 

 

 

배열의 순회

 

1. 인덱스를 신경 쓰지 않는경우 : for-of 문

2. 인덱스의 타입이 중요한 경우 : forEach문 , for(;;) 루프문 (루프 중간에 멈춰야한 경우)

 

타입이 불확실한 경우 for-in 루프는 다른 for 문보다 느리다.

 

결론적으로 숫자 인덱스를 사용하려면 객체보다는 Array, 튜플 혹은 ArrayLike(ArrayLike도 결국에는 키가 문자열이다)를 쓰는 것이 좋다.

 

17. 변경 관련된 오류 방지를 위해 readonly 사용하기

readonly 를 사용할 때 가질 수 있는 특징이 있다.

 

1. 배열의 요소를 읽을 순 있지만, 쓸 순 없다.

2. length 를 읽을 순 있지만 바꿀수는 없다.

3. 배열을 변경하는 pop을 비롯한 다른 메서드 호출 불가능

 

 

readonly에 일반 배열은 선언할 수 있지만, 그 반대는 불가능하다

const a: number[] = [1,2,3];
const b: readonly number[] = a; // 가능
const c:number[] = b // 불가능

 

이러한 속성 때문에 readonly를 호출하는 함수는 무조건 readonly 타입이어야 한다.

 

 

18. 매핑된 타입을 사용하여 값을 동기화하기

interface ScatterProps{
   // the Data
   xs :number[];
   ys:number[];
   
   //Display
   xRange:[number,number];
   yRange:[number,number];
   color:string;
   
   //events
   onClick: (x:number, y:number, index:number) => void;
   
 }

 

산점도(scatter plot)을 그린다고 가정할 때, 디스플레이와 동작을 제어하기 위한 타입이 지정되어 있다.

데이나 디스플레이 속성이 변경되면 다시 그려야 하지만, 이벤트 핸들러가 변경되면 다시 그릴 필요가 없다.

 

그러나, 이벤트 핸들러 Prop이 새 화살표 함수로 설정된다. (useCallback 훅을 사용하여 랜더링할 때 마다 새 함수를 생성하지 않도록 할 수 있다.) 다른 최적화 방안에 대해 생각해볼 때,

 

const shouldUpdate(oldProps:ScatterProps, newProps:ScatterProps){
   let k:key of ScatterProps;
   for (k in oldProps){
      if(oldProps[k]!==newProps[k]){
         if(k!=='onClick') return true;
       }
     }
    return false;
 }

 

만약 새로운 속성이 추가되면 shouldUpdate 함수는 값이 변경 될 때 마다 차트를 그리게 되는데 이것을 보수적 접근법 (실패에 닫힌 방법)⏤ 오류 발생시에 적극적으로 대처하는 방향 이라고 한다. 단점은 너무 자주 그려질 가능성이다.

 

반대로, 실패에 열린 방법은 오류 발생 시에 소극적으로 대처하는 방향을 의미한다.