이펙티브 타입스크립트 (댄 밴더캄 지음) 를 읽고 정리
📍요약
✅JS로 작성한 코드의 확장자를 .ts로 바꿔도 실행 가능하다.
(왜냐면)
✅반면, TS로 작성한 코드의 확장자를 .js로 바꾸면 에러가 발생한다. (타입스크립트 별도의 문법 존재)
📍타입 구문을 통해 타입스크립트에게 코드의 의도를 전달
✅strictNullChecks: false
// tsConfig: {"strictNullChecks":false}
function extent(nums: number[]) {
let min, max;
for (const num of nums) {
if (!min) { // min 이 undefined 이든 0 이든 이 부분이 실행되어버림
min = num;
max = num;
} else {
min = Math.min(min, num);
max = Math.max(max, num);
}
}
return [min, max];
}
✅strictNullChecks: true
// tsConfig: {"strictNullChecks": true}
function extent(nums: number[]) {
let min, max;
for (const num of nums) {
if (!min) {
min = num; // num: undefined 일 수도 있음
max = num;
} else {
min = Math.min(min, num); // Math.min, max은 무조건 number를 매개 변수로 받아야 하는데
max = Math.max(max, num); // 여기의 max 가 number | undefined 라서 문제
// ~~~ Argument of type 'number | undefined' is not
// assignable to parameter of type 'number'
}
}
return [min, max]; // (number | undefined)[]
}
✅타입 에러만 없앤 버전
아직 함수의 근본적인 문제
- 0 탐지 불가
- 빈 배열이 인수로 주어진 경우 [ undefined, undefined ] 반환됨
가 해결되지 않음
// tsConfig: {"strictNullChecks": true}
function extent(nums: number[]) {
let min, max;
for (const num of nums) {
if (!min || !max) { // num = 0 일때 탐지 못함
min = num;
max = num;
} else {
min = Math.min(min, num);
max = Math.max(max, num);
}
}
return [min, max]; // (number | undefined)[]
}
// 문제 해결 X
console.log(extent([1, 0, 3, 2, 65])) // [2, 65]
// 0 num 일 때 3이 min, max 에 할당되어 버려서 그 다음 최솟값인 2가 사용됨
console.log(extent([])) // [undefined, undefined]
✅근본적 문제를 해결
- min 과 max 를 튜플에 넣고 타입에 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;
}
console.log(extent([1, 0, 3, 2, 65])) // [0, 65]
console.log(extent([])) // null
📍Promise의 반환값에 따른 타입 불안정성 해소하기
- 게시글 관련 클래스
- 두번의 네트워크 요청이 응답을 받기 까지 user와 posts 속성은 변화
둘 다 null => 하나만 null => 둘 다 null 아님
- 이부분을 참조하는 코드가 복잡해지는 부정적인 영향을 끼침
interface UserInfo { name: string }
interface Post { post: string }
// declare를 사용한 부분은 컴파일 X
declare function fetchUser(userId: string): Promise<UserInfo>;
declare function fetchPostsForUser(userId: string): Promise<Post[]>;
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() {
// ...?
}
}
- 우선 스태틱 메서드를 정의한 후에 필요한 데이터가 모두 준비된 후에 새 인스턴스를 직접 만들도록 변경
interface UserInfo { name: string }
interface Post { post: string }
declare function fetchUser(userId: string): Promise<UserInfo>;
declare function fetchPostsForUser(userId: string): Promise<Post[]>;
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;
}
}
- user와 posts가 완전히 null 이 아니게 됨
⭐null 인 경우가 필요한 프로퍼티는 프로미스로 바꾸면 안된다
- 코드가 매우 복잡해지며 모든 메서드가 비동기로 바뀌어야 함
⭐프로미스는 fetch 코드를 단순하게 하지만, 데이터를 사용하는 클래스에서는 코드가 복잡해진다