반응형
아이템 11 - 잉여 속성 체크의 한계 인지하기
- 구조적 타입 시스템에서 발생할 수있는 오류를 잡을 수 있도록 '잉여 속성 체크' 수행
- 객체 리터럴을 변수에 할당할 때
- 함수에 매개변수로 전달할 때
- 잉여 속성 체크는 할당 가능 검사와는 별도의 과정
- 타입 단언문을 사용하면 적용되지 않음
- 인덱스 시그니처를 사용해 속성을 예상할 수 있도록 함
interface Room {
numDoors: number;
ceilingHeightFt: number;
}
const r : Room = {
numDoors: 1,
ceilingHeightFt: 10,
elephant: 'present',
} // elephant가 없다고 에러가 발생
구조적 타이핑에 의한 오류
const obj = {
numDoors: 1,
ceilingHeightFt: 10,
elephant: 'present',
}
const r : Room = obj;
obj 타입은 Room타입의 부분 집합이므로 Room에 할당 가능
아이템 12 - 함수 표현식에 타입 적용하기
type DiceRollFn = (sides: number) => number;
const rollDice: DiceRollFn = sides => { };
- 불필요한 코드의 반복을 줄임
- 반복되는 함수 시그니처를 하나의 함수 타입으로 통합
- 함수의 매개변수에 타입 선언을 하는 것 보다, 함수 표현식 전체 타입을 정의하는 것이 안전
아이템 13 - 타입과 인터페이스의 차이점 알기
type Tstate = {
name: string;
capital: string;
}
interface Istate {
name: string;
capital: string;
}
C#에서 비롯된 네이밍, 단순히 차이를 보여주기 위해 작성했으므로 이런 스타일의 네이밍은 지양해야 함
공통
- 인덱스 시그니처(
[key: string]: string
) - 함수타입 (
(x: number) => string;
) - 제네릭 (
type TPair<T>
) - 타입 별칭(
alias
)
차이
- 인터페이스, 타입 서로 확장 가능
- 인터페이스는 유니온 타입 같은 복잡한 타입은 확장할 수 없음
interface IStateWithPop extends TState {
population: number;
}
type TStateWithPop = IState & { population: number; }
타입
- 유니온 타입만 존재 (
type AorB = 'a' | 'b'
) - 튜플과 배열 타입 간결하게 표현
- 인터페이스로 비슷하게 구현할 수 있으나, 비효율적
- concat과 같은 메서드를 사용할 수 없음
인터페이스
- 보강(argument) 가능
- 선언 병합(declaration merging)이라함
interface IState {
a: string;
}
interface IState {
b: string;
}
const c: IState = {
a: 'a',
b: 'b',
} // 정상
아이템 14 - 타입 연산과 제네릭 사용으로 반복 줄이기
- DRY (don’t repeat yourself) 원칙에 따라 함수 뿐만 아니라 타입 정의도 사용할 수 있다.
반복을 제거하는 다양한 방법들
- 같은 함수 타입 시그니처를 사용할 경우의 타입 분리
type HTTPFnction = (url: string, options: Options) => Promise<Response>;
const get: HTTPFunction = (url, options) => { /* ... */ };
const post: HTTPFunction = (url, options) => { /* ... */ };
- 인터페이스 확장을 통해 반복 제거
interface Person {
firstName: string;
lastName: string;
}
interface PersonWithBirthDate extends Person {
birth: Date;
}
- 인터섹션 연산을 통해 속성을 추가하여 확장
interface Person {
firstName: string;
lastName: string;
}
type PersonWithBirthDate = Person & { birth: Date };
부분집합일 경우의 중복제거방법
- 인덱싱을 통해 타입 중복 제거
interface State {
userId: string;
pageTitle: string;
recentFiles: string[];
pageContents: string;
}
type TopNavState = {
userId: State['userId'];
pageTitle: State['pageTitle'];
recentFiles: State['recentFiles'];
};
- 좀 더 발전된 매핑된 타입 형태 ⇒ 배열 필드를 루프 도는 방식
type TopNavState = {
[k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
};
- 위의 같은 패턴 라이브러리를 Pick이라고 하며, Pick은 제네릭 타입이다.
// type Pick<T, K> = { [k in K]: T[k] };
type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
Pick
- 타입의 반복없이 원하는 속성을 정의할 수 있다.
interface SaveAction {
type: 'save';
// ...
}
interface LoadAction {
type: 'load';
// ...
}
type Action = SaveAction | LoadAction;
type ActionType = Action['type'] // 'save' | 'load'
type ActionRec = Pick<Action, 'type'>; // {type: "save" | "load"}
Partial
- 매핑된 타입을 순회하면서 해당 속성이 있는지 찾아 선택적으로 각 속성을 만듦.
- 특정 타입의 부분 집합을 만족하는 타입을 정의.
interface Address {
email: string;
address: string;
}
type MyEmail = Partial<Address>;
/*
interface MyEmail{
email?: string;
address?: string;
}
*/
const me: MyEmail = {}; // 가능
const you: MyEmail = { email: "noh5524@gmail.com" }; // 가능
const all: MyEmail = { email: "noh5524@gmail.com", address: "secho" }; // 가능
typeof 연산자
- 값의 형태에 해당하는 타입을 정의하고 싶을 때 사용
const INIT_OPTIONS = {
width: 640,
height: 480,
color: '#00FF00',
label: 'VGA',
};
/* 이렇게 재정의를 안하기 위해 typeof 연산자 사용
interface Options {
width: number;
height: number;
color: string;
label: string;
}
*/
type Options = typeof INIT_OPTIONS;
ReturnType
- 함수
Type
의 리턴 타입으로 구성된 타입을 생성
function getUserInfo(userId: string) {
// COMPRESS
const name = 'Bob';
const age = 12;
const height = 48;
const weight = 70;
const favoriteColor = 'blue';
// END
return {
userId,
name,
age,
height,
weight,
favoriteColor,
};
}
// Return type inferred as { userId: string; name: string; age: number, ... }
type UserInfo = ReturnType<typeof getUserInfo>;
// Redux
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
// store.getState가 값이기 때문에 typeof로 사용해서 제네릭에 타입으로써 넣어줌.
extends
- 제네릭 타입에서 매개변수를 제한할 수 있는 방법
interface Name {
first: string;
last: string;
}
type DancingDuo<T extends Name> = [T, T];
const dancingDuo = <T extends Name>(x: DancingDuo<T>) => x;
const couple1 = dancingDuo([
{first: 'Fred', last: 'Astaire'},
{first: 'Ginger', last: 'Rogers'}
]);
<T extends string> // T를 string으로 제한
- pick을 좀 더 정확히 사용할 수 있음.
interface Name {
first: string;
last: string;
}
type DancingDuo<T extends Name> = [T, T];
type FirstLast = Pick<Name, 'first' | 'last'>; // OK
type FirstMiddle = Pick<Name, 'first' | 'middle'>;
아이템 15 - 동적 데이터에 인덱스 시그니처 사용하기
[property: string]: string
- 잘못된 키를 포함한 모든 키를 허용
- 특정 키가 필요하지 않음 (
{}
) - 키마다 다른 타입을 가질 수 없음
- 자동완성 미지원
대안
Record
사용 - 키 타입에 유연성을 제공하는 제네릭 타입type Vec3D = Record<'x' | 'y' | 'z', number>
매핑된 타입 사용
type Vec3D = {[k in 'x' | 'y' | 'z']: number}
런타임 때까지 객체의 속성을 알 수 없을 대 인덱스 시그니처 사용
안전한 접근을 위해 인덱스 시그니처 값 타입에
undefined
추가정확한 타입을 사용하는 것이 좋음
아이템 16 - number 인덱스 시그니처보다는 Array, 튜플, ArrayLike를 사용하기
- 타입스크립트는 자바스크립트의 타입 혼란을 바로잡기 위해 숫자 키를 허용하고, 문자열 키와 다른 것으로 인식함.
- 자바스크립트에서 키는 무조건 문자열로 인식
- 하지만 런타임때는 문자열 키로 인식함. ⇒ 타입 체크 시점에서 오류를 잡음.
const xs = [1,2,3]
const keys = Object.keys(xs); // string[]
for (const key in xs) {
key; // string
const x = xs[key]; // number
}
- 인덱스 시그니처에 number를 사용하기보다 Array나 튜플, ArrayLike를 사용하는것이 좋음
아이템 17 - 변경 관련된 오류 방지를 위해 readonly 사용하기
function arraySum(arr: number[]) {
let sum = 0, num;
while ((num = arr.pop()) !== undefined) {
sum += num;
}
return sum;
}
function printTriangles(n: number) {
const nums = [];
for (let i = 0; i < n; i++) {
nums.push(i);
console.log(arraySum(nums));
}
}
// JS는 암묵적으로 함수가 매개변수를 변경하지 않는다고 가정하는데, 이는 타입에 문제가 생길 수 있음.
// 따라서 명시적으로 변경하지 않는다는 것을 알려주는 것이 좋음.
function arraySum(arr: readonly number[]) {
let sum = 0
for (const num of arr) {
sum += num
}
return sum
}
배열은 readonly 배열보다 기능이 많기 때문에, 서브타입이 된다.
const a: number[] = [1,2,3]
const a: readonly number[] = [1,2,3]
const a: number[] = b // 할당 불가
매개변수를 readonly로 선언하는 경우
- 매개변수가 함수 내에서 변경되는지 확인
- 호출하는 쪽에서 함수가 매개변수를 변경하지 않는다는 보장을 받음
- 호출하는 쪽에서 함수에 readlonly 배열을 매개변수로 넣을 수 있음
어떤 함수를 readonly로 선언하면, 해당 함수를 호출하는 다른 함수들도 readonly로 선언해야 함.
라이브러리의 함수를 호출하는 경우는 타입 단언문을 사용해야 함.
기본적으로 readonly는 shallow하게 동작하며
deep readonly는 제네릭을 만들거나 라이브러리를 사용해야 함
아이템 18 - 매핑된 타입을 사용하여 값을 동기화하기
// 보수적(conservative) 접근법, 실패에 닫힌(fail close) 접근법
// 정확하지만 너무 자주 그려질 가능성이 있음
function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps) {
let k: keyof ScatterProps;
for (k in oldProps) {
if (oldProps[k] !== newProps[k]) {
if(k !== 'onClick') return true
}
return false
}
}
// 실패에 열린 접근법
// 차트를 그려야 할 경우 누락될 수 있음
function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps) {
let k: keyof ScatterProps;
return (
oldProps.xs !== newprops.xs ||
oldProps.ys !== newprops.ys ||
oldProps.xRange !== newprops.xRange ||
oldProps.yRange !== newprops.yRange ||
oldProps.color !== newprops.color)
// no check for onClick
)
}
// 타입 체커를 통해 개선
// 매핑된 타입과 객체를 사용
const REQUIRES_UPDATE: {[k in keyof ScatterProps]: boolean} = {
xs: true,
ys: true,
xRange: true,
yRange: true,
color: true,
onClick: false,
}
function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps) {
let k: keyof ScatterProps;
for (k in oldProps) {
if (oldProps[k] !== newProps[k] && REQUIRES_UPDATE[k]) {
return true
}
return false
}
}
- 매핑된 타입을 사용해 관련된 값과 타입을 동기화하도록 해야함
- 인터페이스에 새로운 속성을 추가할 때, 선택을 강제하도록 매핑된 타입을 고려해야 함
아이템 19 - 추론 가능한 타입을 사용해 장황한 코드 방지하기
- 타입 추론이 된다면 명시적 타입 구문은 필요하지 않음
- 비구조화 할당문으로 모든 지역 변수의 타입이 추론되도록 함
- 이상적인 경우 함수/메서드 시그니처에는 타입 구문이 있지만, 함수 내 지역 변수에는 타입 구문이 없음
- 추론될 수 있는 경우라도 객체 리터럴과 함수 반환에서는 타입 명시 / 오류 방지
아이템 20 - 다른 타입에는 다른 변수 사용하기
- 서로 관련이 없는 두 개의 값을 분리
- 변수명을 더 구체적으로 지을 수 있음
- 타입 추론을 향상시키며, 타입 구문이 불필요해짐
- 타입이 좀 더 간결해짐
- let 대신 const로 변수를 선언하므로, 타입 체커가 타입 추론이 쉬워짐
출처: 이펙티브 타입스크립트