Cherry & Cherish
[TypeScript] 타입 주변에 null값 배치하기 본문
이 글은 이펙티브 타입스크립트 '아이템 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[]로 추론된다. 그러나 여기에는 버그와 함께 설계적 결함이 있다.
- 최솟값이나 최댓값이 0인 경우, 값이 덧씌워져 버린다. 예를 들어, extent([0, 1, 2])의 결과는 [0, 2]가 아니라 [1, 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상태다.
-
- 둘다 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 값과 관련된 문제점을 찾아낼 수 있기 때문에 반드시 필요하다.
'Programming > TypeScript' 카테고리의 다른 글
[TypeScript] 문서에 타입 정보를 쓰지 않기 (0) | 2023.04.25 |
---|---|
[TypeScript] 타입 연산과 제너릭 사용으로 반복 줄이기 (0) | 2023.04.22 |
[TypeScript] 타입과 인터페이스의 차이점 알기 (0) | 2023.04.20 |
[TypeScript] 함수표현식에 타입 적용하기 (0) | 2023.04.17 |
[TypeScript] 잉여 속성 체크란 무엇일까? (0) | 2023.04.14 |
Comments