일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 결정 알고리즘
- recoil
- 리터럴 타입
- SSR
- RTK Query
- Cypress
- React
- 투포인터
- 호이스팅
- 이분 검색
- async/await
- Jest
- 타입 좁히기
- TS
- map
- 공변성
- ESlint
- CORS
- autosize
- tailwind
- Promise
- 태그된 유니온
- 반공변성
- 인터섹션
- dfs
- useAppDispatch
- webpack
- 무한 스크롤
- CI/CD
- app router
- Today
- Total
짧은코딩
"모두 null" || "모두 null이 아닌 데이터"로 모델링 하기 본문
타입만으로 어떤 변수가 null이 될 수 있는지 없는지를 표현하기는 어렵다. B에서 A의 값이 나오는 거면 A가 null이 될 수 없을 때 B도
null이 될 수 없다. 반대로 A가 null이 될 수 있으면 B도 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)
// ~~~ Argument of type 'number | undefined' is not
// assignable to parameter of type 'number'
}
}
return [min, max]
}
이 코드는 max가 undefinded인 경우를 고려하지 못해서 에러가 발생했다. 에러를 해결할 수 있는 방법은 min과 max를 한 객체 안에 넣고 null이거나 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;
}
const [min, max] = extent([0, 1, 2])!;
const span = max - min; // OK
반환 타입이 [number, number]이거나 null이 되어 사용하기 쉬워졌다. "!" 단언문을 사용하면 min과 max를 얻을 수 있다.
-if 문을 사용한 경우
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;
}
const range = extent([0, 1, 2]);
if (range) {
const [min, max] = range;
const span = max - min; // OK
}
if문을 사용해서도 제대로 동작하게 구현할 수 있다.
클래스 사용 예제
잘못된 코드
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() {
// ...?
}
}
fetchUser와 fetchPostForUser는 서버로부터 데이터를 가져오는 요청 함수이다. 위 코드에서 두 요청이 실행되는 동안 user, post는 null이다. 어떤 시점에서는 둘 중 하나만 null일 수 있다. 2 * 2 하여 총 4가지의 경우의 수가 존재한다. 이러면 null 체크가 너무 빈번하게 되어 에러가 날 수 있다.
개선한 코드
class UserPosts {
user: UserInfo
posts: Post[]
constructor(user: UserInfo, posts: Post[]) {
this.user = user
this.posts = posts
}
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인 경우가 필요한 속성은 프로미스로 바꾸면 안 된다. 모든 메서드는 비동기로 바꿔줘야 한다.
'TS > 이펙티브 타입스크립트' 카테고리의 다른 글
string보다 구체적 타입 사용하기 (0) | 2023.03.29 |
---|---|
인터페이스의 유니온 사용하기 (1) | 2023.03.27 |
타입 설계의 중요성 (0) | 2023.03.22 |
타입 추론과 문맥 (0) | 2023.03.13 |
비동기는 async 사용 (0) | 2023.03.09 |