[Effective TypeScript] - 타입 설계(1)

이예슬·2022년 12월 4일
0

Effective TypeScript

목록 보기
11/15

Item28. 유효한 상태만 표현하는 타입을 지향하기

  • 유효한 상태와 무효한 상태를 둘 다 표현하는 타입은 혼란을 초래하기 쉽고 오류를 유발하게 된다.
  • 유효한 상태만 표현하는 타입을 지향해야 한다. 코드가 길어지거나 표현하기 어렵지만 결국은 시간이 절약하고 고통을 줄일 수 있다.

Item29. 사용할 때는 너그럽게, 생성할 때는 엄격하게

사용할 때는 너그럽게, 생성할 때는 엄격하게

TCP와 관련하여 존 포스텔(Jon Postel)이 쓴 견고성 원칙(rubustness principle) 또는 포스텔의 법칙에서 따온 말이다. 사용자가 입력할 때는 어떤 값을 받아도 동작할 수 있도록 유연하게 설계하고 내가 수행하는 작업은 보수적으로 설계하라는 원칙인데 이는 타입스크립트에서 타입을 설계할 때도 적용할 수 있다.

함수의 매개변수는 타입의 범위가 넓어도 되지만 결과를 반환할 때는 일반적으로 타입의 범위가 더 구체적이어야 한다. 매개변수 타입의 범위가 넓으면 사용하기 편리하지만 반환 타입의 범위가 넓으면 불편하다. 따라서 함수의 사용성을 높이기 위해서는 반환타입의 타입을 엄격하게 설계해야 한다.

  • 보통 매개변수 타입은 반환 타입에 비해 범위가 넓은 경향이 있다. 선택적 속성과 유니온 타입은 반환 타입보다 매개변수 타입에 더 일반적이다.
  • 매개변수와 반환 타입의 재사용을 위해 기본 형태(반환 타입)와 느슨한 형태(매개변수 타입)를 도입하는 것이 좋다.

Item30. 문서에 타입 정보를 쓰지 않기

코드와 주석의 정보가 맞지 않으면 둘 다 잘못된 것이라고 할 수 있다. 타입스크립트의 타입 구문 시스템은 간결하고 구체적이며 쉽게 읽을 수 있으므로 함수의 입력과 출력의 타입을 코드로 표현하는 것이 주석으로 작성하는 것보다 더 효율적이다. 주석은 누군가 강제하지 않는 이상 코드와 동기화되지 않지만 타입 구문은 타입 체커가 타입 정보를 동기화하도록 강제하므로 타입 정보가 주석보다 정보다 정확히 동기화된다.

그럼에도 불구하고 특정 매개변수에 대한 설명을 추가하고 싶다면 JSDoc의 @param 구문을 사용하면 된다.

변수명에 타입 정보를 넣는 것 또한 좋은 방법이 아니다. 단 단위가 무엇인지 확실하지않다면 추가하여도 좋다.

Item31. 타입 주변에 null 값 배치하기

strictNullChecks 설정을 사용하면 null과 undefined에 대한 많은 오류를 표시함으로서 문제점을 찾아낼 수 있다.

function extent(nums: number[]){
	let min, max;
	for(const num of nums){
		if(!min){
			min = num; 
			max = num;
		} else{
			min = Math.min(min, num);
			max = Math.max(max, num); //❗️number | undefined 형식은 number 형식에 할당될 수 없다. 
	  }
  }
  return [min, max]

}

위 코드는 strictNullChecks 가 꺼져있을 경우에는 타입 체커를 통과하지만 해당 설정을 켜면 몇 가지 에러가 발생한다.

min이 없을 경우만을 체크하기 때문에 max 부분에서 number | undefined 형식의 인수는 number에 할당할 수 없다는 에러가 발생한다.

num이 0이라면 if문에서 false로 취급되므로 값이 덮어씌어지는 현상이 발생한다.

빈 배열을 매개변수로 준다면 [undefined, undefined] 를 반환한다.

이러한 문제를 해결하기 위해서는 min과 max를 한 객체 안에 넣고 단일 객체를 사용하거나 함수 호출의 결과값을 타입 단언문을 사용하거나 분기 처리를 해 줄 수 있다.

  • 한 값의 null 여부가 다른 값의 null 여부에 암시적으로 관련되도록 설계하면 안된다.
  • API 작성시에는 반환 타입을 큰 객체로 만들과 반환 타입 전체가 null이거나 null이 아니게 만들어야 명료한 코드가 될 수 있다.
  • 클래스를 만들 때는 필요한 모든 값이 준비되었을 때 생성하여 null이 존재하지 않도록 하는 것이 좋다.
  • strictNullChecks를 설정하면 코드에 많은 오류가 표시되지만 null 값과 관련된 ㅁ누제점을 찾아낼 수 있기 때문에 반드시 필요하다.

Item32. 유니온의 인터페이스보다는 인터페이스의 유니온을 사용하기

유니온의 인터페이스보다는 인터페이스의 유니온을 사용하는 것이 설계상 더 정확할 수 있다.

interface Layer {
	layout: FillLayout | LineLayout | PointLayout;
	paint: FillPaint | LinePaint | PointPaint; 
} 

//------------------------------
interface FillLayer {
	type: 'fill';
	layout: FillLayout;
	paint: FillPaint;
} 

interface LineLayer {
	type: 'line';
	layout: LineLayout;
	paint: LinePaint;
} 

interface PointLayer {
	type: 'point';
	layout: PointLayout;
	paint: PointPaint;
} 

type Layer = FillLayer | LineLayer | PointLayer;

Layer interface와 같이 타입을 선언하는 것보다는 Layer type과 같이 타입을 선언하는 것이 설계상 오류를 방지할 수 있다. 잘못된 조합으로 섞이는 경우도 방지할 수 있고 유효한 상태만을 표현하도록 타입을 사용할 수 있다. 또한 태그된 유니온을 사용하면 런타임에 어떤 타입의 Layer가 사용되는지 판단할 수 있다. 어떤 데이터 타입을 태그된 유니온으로 표현할 수 있다면 보통 그렇게 하는 것이 좋다.

  • 유니온 타입의 속성을 여러 개 가지는 인터페이스에서는 속성 간의 관계가 분명하지 않기 때문에 실수가 자주 발생하므로 주의해야 한다.
  • 유니온의 인터페이스보다 인터페이스의 유니온이 더 정확하고 타입스크립트가 이해하기도 좋다.
  • 타입스크립트가 제어 흐름을 분석할 수 있도록 타입에 태그를 넣는 것을 고려해야 하며 태그된 유니온은 타입스크립트와 매우 잘 맞기 때문에 자주 볼 수 있는 패턴이다.

Item33. string타입보다 더 구체적인 타입 사용하기

string 타입은 매우 넓은 범위를 가진다. 때문에 string 타입으로 변수를 선언하려 한다면 그보다 더 좁은 타입을 선언할 수 있는지 고려해봐야 한다.

interface Person {
	name: string; 
	age: number; 
	ownSmartPhone: string
} 

type SmartPhone = 'Galaxy' | 'Iphone' | 'BlackBerry' ;

interface Person {
	name: string; 
	age: number; 
	ownSmartPhone: SmartPhone
}

위에 있는 Person 타입보다 아래에 있는 Person 타입을 사용하는 것이 더 유리한 점은 다음과 같다.

  • 타입을 명시적으로 정의함으로써 다른 곳으로 값이 전달되어도 타입 정보가 유지된다.
  • 타입을 명시적으로 정의하고 해당 타입의 의미를 설명하는 주석을 붙여 넣을 수 있다.( 편집기에서 확인 가능)
  • keyof 연산자로 더욱 세밀하게 객체 속성 체크가 가능하다. pluck은 어떤 배열에서 한 필드의 값만 추출하는 함수이다.
    // key의 type이 string으로 너무 넓다.
    const pluck = <T>(records: T[], key: string) => {
    	return records.map(r => r[key]);
    }
    
    // key의 type에 keyof를 사용했다. string 보다는 좁지만 그래도 여전히 넓다. 
    const pluck = <T>(records: T[], key: keyof T) => {
    	return records.map(r => r[key]);
    }
    
    // keyof T의 부분 집합으로 타입을 더 좁혔다. 
    const pluck = <T, K extends typeof T>(records: T[], key: K) => {
    	return records.map(r => r[key]);
    }
    아래와 같이 타입을 작성하게 되면 타입이 정밀해지고 자동 완성 기능을 제공할 수 있게 해준다. 보다 정확한 타입을 사용하면 오류를 방지하고 코드의 가독성도 향상시킬 수 있다. 즉 객체의 속성 이름을 함수 매개변수로 받을 때는 string보다 keyof T를 사용하는 것이 좋다.

Item34. 부정확한 타입보다는 미완성 타입을 사용하기

타입 선언을 세밀하게 만들고자 할 때 시도가 너무 과해 오히려 타입이 부정확해지는 경우가 있다. 또한 타입 선언의 정밀도를 높이려다가 높이지 않았을 때보다 발생하는 메시지의 오류가 부정확해지는 경우도 있다.

  • 타입 안정성에서 불쾌한 골짜기는 피해야 한다. 타입이 없는 것보다 잘못된 것이 더 나쁘다.
  • 정확하게 타입을 모델링할 수 없다면 부정확하게 모델링하지 말아야 한다. 또한 any와 unknown을 구별해서 사용해야 한다.
  • 타입 정보를 구체적으로 만들수록 오류 메시지와 자동 완성 기능에 주의를 기울여야 한다. 정확도뿐 아니라 개발 경험과도 관련된다.

<이펙티브 타입스크립트> Dan Vanderkam, 프로그래밍 인사이트 (2021)

profile
꾸준히 열심히!

0개의 댓글