Cherry & Cherish

[TypeScript] 타입 주변에 null값 배치하기 본문

Programming/TypeScript

[TypeScript] 타입 주변에 null값 배치하기

앵도라지 2023. 4. 27. 15:05

이 글은 이펙티브 타입스크립트 '아이템 31 타입 주변에 null값 배치하기'를 정리한 글입니다.

 

 

  • strictNullChecks 설정을 처음 켜면 null이나 undefined 값 관련된 오류들이 갑자기 나타난다.
  • 이 오류를 해결하기 위해 코드 전체에 if 구문을 추가해야 한다고 생각할 수 있다.
    • 어떤 변수가 null 이 될 수 있는지 없는지를 타입만으로는 명확하게 표현하기 어렵기 때문

 

예를 들어, B가 A로부터 비롯되는 값이라고 하자.

A가 null이 될 수 없을 때, B역시 null이 될 수 없고, 그 반대가 A가 null이 될 수 있다면, B역시도 null이 될 수도 있다.

이 관계들이 겉으로 드러나지 않기 때무에 혼란스럽다.

  • 값이 전부 null이거나, 전부 null이 아닌 경우로 분명히 구분된다면, 값이 섞여있을 때보다 간편하게 다룰 수 있다.
  • 타입에 null을 추가하는 방식으로 모델링하면 된다.
//숫자들의 최솟값과 최댓값을 계산하는 함수
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);
			 // strictNullChecks 설정 켜면 에러발생 
			// 'number | undefined' 형식의 인수는 
			// 'number'형식의 매개변수에 할당될 수 없다.
		}
	}
	return [min, max];
}
  • 위에 코드는 타입 체크를 통과하고 반환 타입은 number[]로 추론된다. 그러나 여기에는 버그와 함께 설계적 결함이 있다.
  1. 최솟값이나 최댓값이 0인 경우, 값이 덧씌워져 버린다. 예를 들어, extent([0, 1, 2])의 결과는 [0, 2]가 아니라 [1, 2]가 된다.
  2. nums 배열이 비어있다면 함수는 [undefined, undefined]를 반환한다.
    • undefined를 포함하는 객체는 다루기 어렵고 절대 권장하지 않는다.

 

해법

  • min과 max를 한 객체 안에 넣고 null이거나 null이 아니게 하면 된다.
function extent(nums: number[] {
	let result: [number, number] | null = null;
	for (const num of nums) {
		if (!result) {
			result = [num, num];
		} else {
			result = [Math.min(num, result[0]), Math.max(num, result[1])];
		}
	}
	return result;
}
  • 이젠 반환타입이 [number, number] | null이 되어서 사용하기가 더 수월해졌다.
  • null 아님 단언(!)을 이용해 min max를 얻을 수 있다.
  • 또는 if 구문으로 체크할 수도 있다.

 

 

클래스에서 생기는 문제

  • null과 null이 아닌 값을 섞어서 사용하면 클래스에서도 문제가 생긴다.
//사용자와 그 사용자의 포럼 게시글을 나타내는 클래스

class UserPosts {
	user : UserInfo | null;
	posts : Post[] | null;
	
	constructor() {
		this.user = null;
		this.posts = null;
	}

	async init(userId : string) {
		return Promise.all([
			async () => this.user = await fetchUser(userId),
			async () => this.posts = await fetchPostsForUser(userId)
		]);
	}

	getUserName() {
		// ...
	}
}
  • 두 번의 네트워크 요청이 로드되는 동안 user와 posts 속성은 null상태다.
    1. 둘다 null / 2,3) 둘 중 하나만 null / 4)둘 다 null 아님
  • 위의 네 가지 경우가 존재한다.
  • 속성 값의 불확실성이 클래스의 모든 메서드에 나쁜 영향을 미친다.
    • 결국 null 체크가 난무하고 버그를 양산하게 된다.

해결법

 class UserPosts {
	user : UserInfo | null;
	posts : Post[] | null;
	
	constructor(user: UserInfo, posts: Post[]) {
		this.user = null;
		this.posts = null;
	}

	static async init(userId : string) : Promise<UserPosts> {
		const [user, posts] = await Promise.all([
			fetchUser(userId).
			fetchPostsForUser(userId)
		]);
		return new UserPosts(user, posts);
	}

	getUserName() {
		return this.user.name;
	}
}
  • 이제 UserPosts 클래스는 완전히 null이 아니게 되었고, 메서드를 작성하기 쉬워졌다.
  • 물론 이 경우에도 데이터가 부분적으로 준비되었을 때 작업을 시작해야 한다면, null과 null이 아닌 경우의 상태를 다루어야 한다.

 

 

요약

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