작성된 코드를 체크하는 정적 분석 시점에 변수는 ‘가능한' 값들의 집합인 타입을 가진다. 이 과정에서 타입을 명시하지 않았을 때, 타입 체커는 타입을 결정해야 한다. 이 말은 지정된 단일 값을 가지고 할당 가능한 집합을 유추해야하는데 이를 ‘넓히기’ 라고 한다.
interface Vector3 {
x: number;
y: number;
z: number;
}
const getComponent = (vector: Vector3, axis: 'x' | 'y' | 'z') => {
return vector[axis];
}
// x가 현재 string으로 추론되어
let x = 'x';
let vec = {x:10, y:20, z: 30};
getComponent(vec, x); // 여기서 에러 발생!
이런 넓히기의 과정을 제어하는 방법
const x = ‘x’
// 타입이 ‘x’)따라서 이러한 부분을 해결하려면 타입 추론의 강도를 직접 제어해야 함.
타입 추론의 강도를 직접 제어하여 타입스크립트의 기본 동작을 재정의 하는 방법
const v: { x: 1|3|5 } = { x: 1 }; // 타입이 {x: 1|3|5; }
as const
사용최근 벨로퍼트 redux를 보고 있는데 거기서도 이러한 문법을 사용(action 타입과 action 생성 함수를 정의하고 타입으로 넘겨줄때)
const SET_DIFF = 'counter/SET_DIFF' as const;
const INCREASE = 'counter/INCREASE' as const;
const DECREASE = 'counter/DECREASE' as const;
export const setDiff = (diff: number) => ({ type: SET_DIFF, payload: diff });
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
type CounterAction = ReturnType<typeof setDiff> | ReturnType<typeof increase> | ReturnType<typeof decrease>;
export default function counter(state: CounterState = initalState, action: CounterAction) {
...
타입 좁히기는 타입스크립트가 넓은 타입으로부터 좁은 타입으로 진행하는 과정이다.
가장 일반적인 예시로 돔요소를 가져와 null
인지 체크하는 과정
타입을 좁히는 방법
const isDefined = <T>(x: T | undefined): x is T => {
return x !== undefined;
};
위와 같이 반환하는 쪽에 x is T는 반환이 true인 경우
타입 체커에게 매개변수의 타입을 좁힐 수 있다고 알려준다.
객체를 생성할 때는 속성을 하나씩 추가하기보다는 여러 속성을 포함해서 한꺼번에 생성해야 타입 추론에 유리하다. 그러나 객체를 반드시 각각 나누어서 만들어야한다면 다음의 방법을 사용할 수 있다.
객체 전개 연산자로 하나의 속성을 조건부 연산으로 추가하면 타입이 선택적 속성을 가진것으로 추론된다. 그러나 여러개의 속성을 추가하면 이는 유니온으로 추론된다. 이 경우 선택적 필드 방식으로 표현하려면 아래와 같이 헬퍼 함수를 사용하자.
// 헬퍼함수
const addOptional = <T extends object, U extends object>(a: T, b: U | null): T & Partial<U> => {
return { ...a, ...b };
};
declare let hasDates: boolean;
const nameTitle = { name: 'Khufu', title: 'Pharaoh' };
const pharaoh = addOptional(nameTitle, hasDates ? { start: -2589, end: -2566 } : null);
pharaoh.start // 정상, 타입이 number | undefined
가끔 객체나 배열을 변환해서 새로운 객체나 배열을 생성하려는 경우 내장된 함수형 기법 또는 로대시 같은 유틸리티 라이브러리를 사용하는 것이 ‘한꺼번에 객체 생성하기' 관점에서 보면 옳다.
const borough = {name: 'Brooklyn', location: [40.688, -73.979]};
const loc = borough.location;
위와 같은 loc이 별칭이다.
별칭 사용시 주의할점
null
값을 추가하는 것이 좋다.객체 속성에서 타입스크립트 제어 흐름 분석을 주의해야 한다.
function fn(p: Polygon) { /* ... */ }
polygon.bbox // 타입이 BoundingBox | undefined
if (polygon.bbox) {
polygon.bbox // 타입이 BoundingBox
fn(polygon);
polygon.bbox // 타입이 BoundingBox
}
이 예제에서
fn
함수가 polygon의 속성을 직접 수정한다면 그 이후의 타입이 달라질 가능성이 있다. 따라서 지역변수(bbox)로 비구조화 할당으로 꺼내서 사용하면 타입을 정확하게 유지할 수 있다. 그러나 이경우도 polygon.bbox의 값과 같게 유지되지 않을 수 있다.
→ 함수 호출이 객체 속성의 타입 정제를 무효화할 수 있다는 점을 주의하자!
콜백보다는 프로미스나 async/await를 사용하자.
Promise.all
의 경우 await
와 구조 분해 할당이 찰떡궁합이다.
const fetchPages = async () => {
const [response1, response2, response3] = await Promise.all([
fetch(url1),
fetch(url2),
fetch(url3),
]);
...
};
또한 프로미스를 사용하면 타입 구문 없이 모든 타입 추론이 제대로 동작한다.(Promise<Response>
)
프로미스 생성 시 async/await 사용의 이점
따라서 어떤 함수가 프로미스를 반환한다면 async로 선언하는 것이 좋다.
타입스크립트는 타입을 추론할때 값이 존재하는 곳의 문맥까지도 살핀다.
그러나 값을 변수로 분리한 경우
type Language = 'JS' | 'TS' | 'Python';
const setLanguage = (language: Language) => {}
let language = 'JS';
setLanguage(language);
이렇게 되면 할당시점에 타입을 추론하므로 string
으로 추론하게 된다.
이를 해결하는 2가지 방법
let language: Language = ‘JS’
const language = ‘JS’
문맥과 값을 분리한 경우 발생할 수 있는 문제를 해결하는 방법
튜플
as const
로 상수 문맥을 제공하며 전달되는 인수부분에 readonly
타입 추가객체
콜백
상수 단언 사용 시 타입 정의에 실수가 있다면 오류는 타입 정의가 아닌 호출되는 곳에서 오류가 발생하므로 주의해야한다.
함수형 기법들을 TS와 함께 사용하면 타입 정보가 그대로 유지되면서 타입 흐름이 계속 전달되므로 좋다.
순수 JS로 절차형, 함수형보다 Lodash같은 유틸리티 라이브러리를 사용하면 타입 구문없이 사용가능한 경우가 많다. (ex: Lodash의 Dictionary 타입)
순수 map 대신 _.map을 사용하는 이유로 콜백을 전달하는 대신 속성의 이름을 전달할 수 있기 때문
→ 타입 흐름을 개선하고, 가독성을 높이고, 명시적인 타입 구문의 필요성을 줄이기 위해 직접 구현하기보다는 내장된 함수형 기법과 Lodash 같은 유틸리티 라이브러리를 사용하자.
효과적인 타입 설계를 위해선, 유효한 상태만 표현할 수 있는 타입을 만들어 내는 것이 중요하다.
// 이렇게만 상태를 나누고 이에따른 함수를 작성하기보다.
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 };
}
따라서 어떤 값들을 포함하고 어떤 값들을 제외할지 신중하게 생각하고, 유효한 상태만 표현하는 타입을 지향하자!
포스텔의 법칙
함수의 매개변수는 타입의 범위가 넓어도 되지만, 결과 반환시 타입의 범위가 구체적이어야 한다.
이를 고려하기 위해 매개변수와 반환 타입의 재사용을 위해서 기본 형태(반환 타입)와 느슨한 형태(매개변수 타입)를 도입하는 것이 좋다.