Closed moonheekim0118 closed 2 years ago
콜백보다는 프로미스가 코드를 작성하기 쉽고, 타입을 추론하기도 쉽다.
Promise.all
과 Promise.race
function timeout(millis: number): Promise<never> {
return new Promise((resolve, reject) => {
setTimeout(() = reject('timeout'), millis);
});
}
function fetchWithTimeout(url: string, ms: number) {
return Promise.race([fetch(url), timeout(ms)]);
}
// fetchWithTimeout의 반환 타입은 Promise<Response>
// 왜?
// Promise.race의 반환 타입은 입력 타입들의 유니온
// => Promise<Response | never> 인데, 공집합 never와의 유니온은 아무 효과가 없음
// => Promise<Response>
프로미스를 직접 생성하기보다는 async/await을 사용하는 것이 좋다.
async 함수는 항상 프로미스를 반환하도록 강제된다. 함수는 항상 동기 또는 항상 비동기로 실행되어야 하며, async 함수를 사용하면 비동기 함수로 통일하도록 강제하는 데 도움이 된다.
const getNumber1 = async () => 42; // 타입은 () => Promise<number>
const getNumber2 = () => Promise.resolve(42) // 타입은 () => Promise<number>
타입스크립트는 일반적으로 값이 처음 등장할 때 타입을 결정한다. 값을 변수로 분리하였을 때 변수의 할당 시점에 타입이 추론된다는 점에 유의한다.
이때 오류가 발생한다면 타입 선언을 추가하거나, 더 정확한 타입 추론이 가능하도록 해야 한다.
type Language = 'JavaScript' | 'TypeScript' | 'Python';
function setLanguage(languate: Language) { /* ... */ }
let language = 'JavaScript'; // 타입은 string
setLanguage(language);
// 오류! 'string' 형식의 인수는 'Language' 형식의 매개변수에 할당될 수 없습니다.
// 해결방안 1
// 타입 선언에서 변수의 가능한 값을 제한하기
let language: Language = 'JavaScript'; // 타입은 Language
setLanguage(language); // 정상
// 해결방안 2
// const를 사용하여 값이 변경되지 않음을 타입 체커에 알려주기
const language = 'JavaScript'; // 타입은 문자열 리터럴 'JavaScript'
setLanguage(language); // 정상
as const
는 문맥 손실과 관련된 문제를 해결할 수 있다. 그러나 타입 정의에 실수가 있는 경우에도, 오류는 사용되는 곳에서 발생하므로 오류의 근본적인 원인을 파악하기 어려운 문제가 생길 수 있다. 따라서 변수가 정말 상수일 때 상수단언(as const)를 사용한다.
// 튜플 사용 예시
// [number, number] 형식의 매개변수를 가지는 함수가 있을 때,
const loc = [10, 20];
// 타입은 number[] => 오류! 매개변수에 할당될 수 없다.
const loc: [number, number] = [10, 20];
// 타입은 [number, number] => 정상
const loc = [10, 20] as const;
// 타입은 'readonly [10, 20]'
// => 오류! 'readonly' 이며 변경가능한 형식 '[number, number]'에 할당할 수 없다.
// => 1. 함수 시그니처에 readonly를 붙이거나 2. 바로 위의 타입 구문 사용하기
효과적으로 타입을 설계하려면, 유효한 상태만 표현할 수 있는 타입을 만드는 것이 중요하다.
interface State {
pageText: string;
isLoading: boolean;
error?: string;
}
function renderPage(state: State) {
if (state.error) {
return `Error! Unable to load ${currentPage}: ${state.error}`;
}
else if (state.isLoading) {
return `Loading ${currentPage}...`;
}
return `<h1>${currentPage}</h1>\n${state.pageText}`;
}
위는 애플리케이션의 상태를 나타내는 코드다. 여기에는 필요한 정보가 부족하다는 문제가 있다. 에러 상태와 로딩 상태를 정확히 구분하기 어렵고, 두 속성이 충돌할 수 있다. isLoading이 true이면서 error 값이 존재하면 로딩 중인지 오류 발생 상태인지 명확히 구분할 수 없다. 이는 정확히 구분이 어려운 무효한 상태다. 유효한 상태와 무효한 상태를 모두 표현하는 타입은 혼란을 초래하고 오류를 유발하기 쉽다.
다음과 같이 무효한 상태를 허용하지 않도록 제대로 표현할 수 있다. 현재 페이지가 무엇인지 명확하고, 모든 요청은 하나의 상태만 가진다.
interface RequestPending {
state: 'pending';
}
interface RequestError {
state: 'error';
error: string;
}
interface RequestSuccess {
state: 'ok';
pageText: string;
}
type RequestState = RequestPending | RequestError | RequestSuccess;
interface State {
currentPage: string;
requests: { [page: string]: RequestState };
}
function renderPage(state: State) {
const { currentPage } = state;
const requestState = state.requests[currentPage];
switch (requestState.state) {
case 'pending':
return `Loading ${currentPage}...`;
case 'error':
return `Error! Unable to load ${currentPage}: ${requestState.error}`;
case 'ok':
return `<h1>${currentPage}</h1>\n${state.pageText}`;
}
}
interface LngLat { lng: number; lat: number; }; // 기본 형태
type LngLatLike = LngLat | { lon: number; lat: number; } | [number, number]; // 느슨한 형태
// 기본 형태
interface Camera {
center: LngLat;
zoom: number;
bearing: number;
pitch: number;
}
// 느슨한 형태
interface CameraOptions extends Omit<Partial<Camera>, 'center'> {
center?: LngLatLike;
}
type LngLatBounds =
{northeast: LngLatLike, southwest: LngLatLike} |
[LngLatLike, LngLatLike] |
[number, number, number, number];
declare function setCamera(camera: CameraOptions): void;
declare function viewportForBounds(bounds: LngLatBounds): Camera;
주석과 변수명에 타입 정보를 적는 것은 피한다. 타입스크립트의 타입 구문 시스템은 간결하고, 구체적이고 쉽게 읽을 수 있기 때문에, 타입스크립트 타입으로 함수의 입출력을 표현하는 것이 주석보다 더 나은 방법이다. 주석은 누군가 강제해야만 동기화되지만, 타입스크립트 타입 구문으로 작성하면는 타입 체커가 타입 정보를 알아서 동기화해준다. 변수명에도 타입 정보를 넣기보다 타입 구문으로 타입을 명시하는 것이 좋다.
단위가 있는 숫자들의 경우, 변수명 또는 속성 이름에 단위를 포함하는 것을 고려한다. timeMS는 time보다 훨씬 명확하다.
interface Point { x : number ; y : number ;}
const pt : Point = {}; -> 에러 발생! x, y가 존재하지 않는다
const pt = { } as Point; -> 타입 단언을 사용하게 되면, 타입 체커문에서 통과할 수 있다
pt.x = 4;
pt.y = 2;
작은 객체들을 조합해서 큰 객체를 만들어야 하는 경우 객체 전개 연선자를 활용하자
const pt = {x:3, y:4};
const id = { name: 'Pythagoras'};
const namePoint = {...pt, ...id};
const pt0 = { };
const pt1 = {...pt0, x: 3};
const pt : Point = {...pt1, y:4};
조건부 속성을 추가할 때에, null 또는 {}로 객체 전개를 사용하기
declare let hasMiddle: boolean;
const firstLast = {first: 'Harry', last: 'Truman'};
const president = {...firstLast, ...(hasMiddle ? {middle: 'S'} : {})};
-> president의 추론된 타입
const president : {
middle?: string;
frist: string;
last :string;
}
비동기 동작을 콜백을 사용했을 때의 단점
프로미스와 async await를 사용해야하는 이유
Promise.race를 사용하여 프로미스에 타임아웃 추가하기
function timeout(mills: number) : Promise<never> {
return new Promise((resolve, reject) => {
setTimeout(() => reject('timeout'),mills);
});
}
async function fetchWithTimeout(url:string, ms:number){
return Promise.race([fetch(url),timeout(ms)]);
} -> 반환 타입을 지정하지 않아도, Promise<Response>로 추론이 가능하다
프로미스보다 async await를 사용해야하는 이유
Follow me, Come and get illusion~ 🎵 카리나는 신이에요 😭
타입스크립트는 타입을 추론할 때 값이 존재하는 곳의 문맥까지 살핀다.
type Language = 'JS' | 'TS' | 'Python';
function setLanguage(language: Language) { console.log(language) }
setLanguage('JS') // OK
let language = 'JS';
setLanguage(language) // Error
// Argument of type 'string' is not assignable to parameter of type 'Language'.
값을 변수로 분리해 내면, 타입스크립트는 할당 시점에 타입을 추론한다. language는 string으로 추론되었고, 이는 Language 타입에 할당이 불가능해 오류가 발생한다
let language: Language = ‘JS’
)const language = ‘JS’
)function panTo(where: [number, number]) { /** */ }
panTo([10, 20]) // OK
const loc = [10, 20];
panTo(loc) // Error
// Argument of type 'number[]' is not assignable to parameter of type '[number, number]'.
// Target requires 2 element(s) but source may have fewer.
타입스크립트는 loc를 number[]로 추론한다. 배열의 길이를 알 수 없기 때문에 튜플 타입에 할당할 수 없다.
const loc: [number, number] = [10, 20]
)상수 문맥 제공하기
function panTo(where: [number, number]) { /** */ }
const loc = [10, 20] as const;
panTo(loc) // Error
// Argument of type 'readonly [10, 20]' is not assignable to parameter of type '[number, number]'.
// The type 'readonly [10, 20]' is 'readonly' and cannot be assigned to the mutable type '[number, number]'.
function panTo(where: readonly [number, number])
)객체의 경우에도 튜플과 유사하다. 타입 선언을 추가하거나, 상수 단언(as const)를 사용하여 해결하자
콜백을 상수로 뽑아내면 문맥이 소실된다.
function withCallback (fn: (n1: number, n2: number) => void) { fn(1,3) }
withCallback((a, b) => { console.log(a+b) }) // a: number, b: number로 추론된다
function withCallback (fn: (n1: number, n2: number) => void) { fn(1,3) }
const fn = (a, b) => { console.log(a+b) } // Parameter 'a' implicitly has an 'any' type.
withCallback(fn)
유효한 상태와 무효한 상태를 모두 표한하는 타입은 혼란스럽고 오류를 유발한다.
유효한 상태만 표현하는 타입을 지향하자
함수의 매개변수는 타입의 범위가 넓어도 되지만, 결과를 반환할 때는 일반적으로 타입의 범위가 구체적이어야 한다. 매개변수 타입의 범위가 넓으면 사용하기 편리하지만, 반환 타입의 범위가 넓으면 불편하다. 사용하기 편리한 API일수록 반환 타입이 엄격하다.
코드와 주석의 정보가 맞지 않을 수 있다. 타입스크립트의 타입 구문 시스템은 간결하고, 구체적이고, 쉽게 읽을 수 있도록 설계되었기 때문에 코드로 표현하는 것이 주석보다 더 나은 방법이다.
값이 null이거나 전부 null이 아닌 경우로 구분된다면 값을 다루기 쉬워진다. 타입에 null을 추가하는 방식으로 이러한 경우를 모델링 할 수 있다.
strictNullChecks 설정을 켜 null이나 undefined 관련 오류들을 확인해보자.
function extend(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'.
// Type 'undefined' is not assignable to type 'number'.
}
}
return [min, max];
}
min이 undefined인 경우만 제외했고, max에대해서는 제외하지 않았다. max에 대한 체크를 추가할 수도 있겠지만 더 좋은 방법을 찾아보자
function extend(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문으로 확인하여 값을 얻을 수도 있다.
class UserPosts {
user: UserInfo | null;
post: Post[] | null;
constructor() {
this.user = null;
this.post = null;
}
async init(userId: string) {
return Promise.all([
async () => this.user = await fetchUser(userId);
async () => this.post = await fetchPostsForUser(userId);
]);
}
getUserName() {
// user, post가 null이거나 아니거나 다양한 경우가 존재
}
}
클래스를 만들 때는 필요한 모든 값이 준비되었을 때 생성하여 null이 존재하지 않도록 하는 것이 좋다.
속성값의 불확실성은 모든 메서드에 나쁜 영향을 미친다. null 체크가 난무하도록 하는 것이 아니라 설계를 바꾸어 null인 경우와 null이 아닌 경우의 상태를 다뤄야 한다.
class UserPosts {
user: UserInfo;
post: Post[];
constructor(user: UserInfo, posts: Post[]) {
this.user = user;
this.post = posts;
}
static async init(userId: string): Promise<UserPosts> {
cosnt [user, posts] = await Promise.all([
fetchUser(userId);
fetchPostsForUser(userId);
]);
return new UserPosts(user, posts)
}
getUserName() {
//
}
}
null인 경우가 필요한 속성은 Promise로 바꾸면 안된다. 코드가 복잡해지며 모든 메서드가 비동기로 바뀌어야 한다.
interface State {
pageText: string;
isLoading: boolean;
error?:string;
}
위와 같은 타입이 있다면..
interface RequestPending{
state: 'pending';
}
interface RequestError {
state: 'error';
error: string;
}
interface RequestSuccess{
state: 'ok';
pageText: string;
}
type RequestState = RequestPending | RequestError | RequestSuccess;
interface State {
currentPage: string;
requests: {[page:string] : RequestState}
}
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];
}
[undefined,undefined]
를 반환한다.function extent(nums:number[]){
let result: [number,number] | null = null;
for(const num of nums){
if(!result){
result = [num,num];
} else {
result = [ Math.min(result[0],num), Math.max(result[1],num) ]
}
}
return result;
}
extent 의 결괏값으로 단일 객체를 사용함으로써 설계 개선
따라서 타입스크립트가 null 값 사이의 관계 (min이 null 이면 max 도 null , vice versa)를 안다.
즉 한 값의 null 여부가 다른 값의 null 여부에 암시적으로 관련되도록 설계하지 말기
API 작성 시에는 반환 타입을 큰 객체로 만들고 반환 타입 전체가 null 이거나 null 이 아니게 만들어야 한다.
클래스를 만들 때는, 필요한 모든 값이 준비 되었을 때 생성하여 null 이 존재하지 않도록 하자.
interface Layer {
layout: FillLayout | LineLayout | PointLayout;
paint: FillPaint | LinePaint |PointPaint;
}
interface FillLayer {
layout: FillLayout;
paint: FillPaint;
}
interface LineLayer {
layout: LineLayout;
paint: LinePaint;
}
interface PointLayer {
layout: PointLayout;
paint: PointPaint;
}
type Layer = FillLayer | LineLayer | PointLayer;
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;
interface Person {
name: string;
placeOfBirth?: string;
dateOfBirth?: number;
}
interface Person { name: string; birth ?: { place: string; date: number; } }
- 위처럼 두개의 속성을 하나의 객체로 모으자.
- null 값을 경계로 두는것과 비슷하구만!
# string 타입보다 더 구체적인 타입 사용하기
1. 변수의 범위를 보다 정확하게 표현하고 싶다면 string 타입 보다는 문자열 리터럴 타입의 유니온 타입을 사용합시다.
2. 객체 속성 이름을 함수 매개변수로 받을 때는 string 보다 keyof T 를 사용합시다.
```ts
function pluck(records: any[], key:string): any[]{
return records.map(r=>r[key]);
}
function pluck<T>(records: T[], key:string): any[]{
return records.map(r=>r[key]);
}
function pluck<T>(records: T[], key: keyof T): T[keyof T] []{
return records.map(r=>r[key]);
}
T[keyof T][]
타입은 (string | Date) []
이 된다.(string | Date) []
인거면 너무 넓다!function pluck<T, K extends keyof T>(records: T[], key: K): T[K] []{
return records.map(r=>r[key]);
}
오늘 타입스크립트 스터디 필수 참고 자료 ‼️
우리 회원님들 행복한 추석되세요 🌕