타입이 명시된 변수에 객체 리터럴을 할당할 때 타입스크립트는 해당 타입의 속성이 있는지, 그리고 '그 외의 속성은 없는지' 확인합니다.
interface User {
name: string;
age: number;
}
const u: User = {
name: 'LeeJS',
age: 28,
tell: 1234 // 객체 리터럴은 알려진 속성만 지정할수 있으며 'User' 형식에 'tell'이(가) 없습니다.
}
변수에 타입을 선언함과 동시에 객체 리터럴로 만들게 되면 잉여 속성이 체크 됩니다.
const leejs = {
name: 'LeeJS', // name: string;
age: 28, // age: number;
tell: 1234 // tell: number;
}
const j: User = leejs
구조적 타이핑이라는 관점에서 봤을 때 해당 예시는 적절해 보입니다. (j 변수에 leejs 변수가 정상적으로 할당.)
해당 leejs 타입은 User 타입의 부분 집합을 포함하므로, User에 할당 가능하면 타입 체커도 통과합니다.(임시 변수를 도입)
두 예시 모두 할당 가능 검사를 통과했습니다. 그러나 잉여 속성 체크는 1번 예시만 동작하고 2번 예시는 동작하지 않습니다.
⭐️ 여기서 알려주는 부분은 할당 가능 검사와 잉여 속성 체크가 별도의 과정이라는 것입니다.
function join(user: User) {
// ... something
}
join(j);
join({
name: 'jong',
agee: 30, // 객체 리터럴은 알려진 속성만 지정할 수 있지만 'User' 형식에 'agee'가 없습니다. 'age'를 쓰려고 했습니까?
gender: 'M', // 'User' 형식에 'gender'가 없습니다.
})
User타입이 된 j 변수는 구조적 타이핑 관점에서 함수에 인자로 넘길 수 있습니다.
❗️ 그러나 새롭게 객체 리터럴로 만든 변수는 잉여 속성 체크에 의해 에러가 발생합니다.
따라서, 잉여 속성 체크는 특정 타입에 객체 리터럴을 생성하여 할당할 때 동작 하는것을 알 수 있습니다.
잉여 속성 체크는 필요한 속성 이외의 속성들을 체크하기 때문에 엄격한 객체 리터럴 체크라고도 불립니다.
const userLee = { name: 'lee', age: 12, tell: 1234 } as User; // 정상
interface Options {
mode: boolean;
[ortherOptions: string]: unkown;
}
const o: Options = { mode: true }; // 정상
interface ListOptions {
title?: string;
text?: string;
image?: string;
}
const opts = { images: 'img url' };
const o: ListOptions = opts;
// ~ '{ images: 'img url' }' 유형에 'ListOptions' 유형과 공통적인 속성이 없습니다.
구조적 관점에서 ListOptions 타입은 모든 속성이 선택적이므로 모든 객체를 포함할 수 있습니다.
이런 약한 타입에 대해서는 타입스크립트는 값 타입과 선언 타입에 공통된 속성이 있는지 확인하는 별도의 체크를 수행합니다.(공통 속성 체크)
공통 속성 체크
요약
📌 객체 리터럴을 변수에 할당하거나 함수에 매개변수로 전달 할 때 잉여 속성 체크가 수행됩니다.
📌 잉여 속성 체크에는 한계가 있습니다. 임시 변수를 도입하면 잉여 속성 체크를 건너뛸 수 있다는 점을 기억해야 합니다.
function user1(id: number): number {/* ... */} // 문장
const user2 = function(id: number): number {/* ... */} // 표현식
const user3 = (id: number): number => {/* ... */} // 표현식
타입스크립트에서는 함수 표현식을 사용하는 것이 좋습니다.
함수의 매개변수부터 반환값까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용 할 수 있다는 장점이 있기 때문입니다.
⭐️ 함수 타입으로 선언하여 함수 표현식에 재사용
type UserJoin = (id: number) => number;
const user: UserJoin = id => {/* ... */};
type BinaryFn = (a: number, b: number) => number;
const add: BinaryFn = (a, b) => a + b;
const sub: BinaryFn = (a, b) => a - b;
const mul: BinaryFn = (a, b) => a * b;
const div: BinaryFn = (a, b) => a / b;
⭐️ 다른 함수의 시그니처를 참조하려면 typeof fn을 사용
declare function fetch(
input: RequestInfo, init?: RequestInit
): Promise<Response>;
const checkedFetch: typeof fetch = async (input, init) => {
const response = await fetch(input, init);
if (!response.ok) {
throw new Error('Request failed: ' + response.status);
}
return response;
}
함수 전체에 타입(typeof fetch)을 적용했습니다. 이는 타입스크립트가 input과 init의 타입을 추론할 수 있게 해 줍니다.
❗️ throw 대신 return을 사용했다면, 타입스크립트는 그 실수를 잡아냅니다.
요약
📌 매개변수나 반환 값에 타입을 명시하기보다는 함수 표현식 전체에 타입 구문을 적용하는 것이 좋습니다.
📌 같은 타입 시그니처를 반복적으로 사용할 경우 함수 타입을 분리해 내거나 이미 존재하는 타입을 확인합니다.
📌 다른 함수의 시그니처를 참조하려면 typeof fn을 사용합니다.
type TState = {
name: string;
capital: string;
}
interface IState{
name: string;
capital: string;
}
const wyoming: TState = {
name: 'Lee',
capital: 'che',
population: 500
// ~~~~~~~~~~~~~~~ ... 형식은 'TState' 형식에 할당할 수 없습니다.
// 개체 리터럴은 알려진 속성만 지정할 수 있으며
// 'TState' 형식에 'population' 이 없습니다.
}
type TDict = { [key: string]: string };
interface IDict {
[key: string]: string;
}
type TFn = (x: number) => string;
interface IFn {
(x: number): string;
}
const toStrT: Tfn = x => '' + x; // 정상
const toStrI: IFn = x => '' + x; // 정상
type TPair<T> = {
first: T;
second: T;
}
interface IPair<T> {
first: T;
second: T;
}
interface IStateWithPop extends TState {
population: number;
}
type TStateWithPop = IState & {population: number;};
❗️ 인터페이스는 유니온 타입 같은 복잡한 타입을 확장하지는 못합니다. 복잡한 타입을 확장하고 싶다면 타입과 &를 사용해야 합니다.
class StateT implements TState {
name: string = '';
capital: string = '';
}
class StaetI implements IState {
name: string = '';
capital: string = '';
}
type AorB = 'a' | 'b';
interface IState {
name: string;
capital: string;
}
interface IState {
population: number;
}
const wyaming: IState = {
name: 'Lee',
capital: 'Che',
populataion: 500
} // 정상
위에 예제처럼 속성을 확장하는 것을 '선언 병합(declaration merging)'이라고 합니다.
복잡한 타입이라면 고민할 것도 없이 타입별칭을 사용하는 것이 좋습니다.
타입과 인터페이스 모두 표현 할 수 있는 간단한 객체 타입이라면 "일관성과 보강의 관점"에서 고려해 봐야 합니다.
그러나 프로젝트 내부적으로 사용되는 타입에 선언 병합이 발생하는 것은 잘못된 설계입니다. 따라서, 그럴 때는 타입을 사용해야 합니다.
DRY(don't repeat yourself) 원칙 : 같은 코드를 반복하지 말라는 뜻입니다.
function distance(a: {x: number, }, b: {x: number, y: number}) {
return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
}
// 위에 코드에 이름을 붙일 경우
interface Point2D {
x: number;
y: number;
}
function distance(a: Point2D, b: Point2D){ /* .... */ }
interface Person {
firstName: string;
lastName: string;
}
interface PersonWithBirthDate extends Person {
birth: Date;
}
이미 존재하는 타입을 확장하는 경우에, 일반적이지는 않지만 인터섹션 연산자(&)를 쓸 수도 있습니다.
(유니온 타입(확장할 수 없는)에 속성을 추가하려고 할 때 유용합니다.)
type PersonWithBirthDate = Person & {birth: Date};
const INIT_OPTIONS = {
width: 100,
height: 100,
color: '#ffffff',
label: 'VGA',
};
type Options = typeof INIT_OPTIONS;
1. Pick
interface State {
id: string;
title: string;
file: string[];
contents: string;
}
type TopNavState = {
id: State['id'];
title: State['title'];
file: State['file'];
}
// 위에 방식에서 '매핑된 타입'을 사용하면 좀더 효율적입니다.
type TopNavState = {
[k in 'id' | 'title' | 'file']: State[k]
};
// Pcik을 사용할 경우
type ToNavState = Pick<State, 'id' | 'title' | 'file'>;
⭐️ 매핑된 타입은 배열의 필드를 루프 도는 것과 같은 방식입니다. 이 패턴은 표준 라이브러리에서 Pick 이라고 합니다.
type Pick<T, K> = { [k in K]: T[k] };
2. Partial
생성하고 난 다음에 업데이트가 되는 클래스를 정의한다면,
update 메서드 매개변수의 타입은 생성자와 동일한 매개 변수이면서, 타입 대부분이 선택적 필드가 됩니다.
interface Options {
width: number;
height: number;
color: string;
label: string;
}
interface OptionsUpdate {
width?: number;
height?: number;
color?: string;
label?: string;
}
class UIWidget {
constructor(init: Options) {/* ... */}
update(opctions: OptionsUpdate) {/* ... */}
}
// 매핑된 타입과 keyof를 사용
type OptionUpdate = {[k in keyof Options]?: Options[k]} // keyof는 타입을 받아서 속성 타입의 유니온을 반환합니다.
// Partial 사용할 경우
class UIWidget {
constructor(init: Options) {/* ... */}
update(opctions: Partial<Options>) {/* ... */}
}
3. ReturnType
함수나 메서드의 반환 값에 명명된 타입을 만들고 싶을 수도 있습니다.
function getInfo(id: string) {
//...
return {
id,
name,
age,
height,
width
};
}
// 추론된 반환 타입은 {id: string, name: string, age: number, ...}
// 이때는 조건부 타입이 필요합니다. 이런 경우 ReturnType 제너릭이 정확히 들어맞습니다.
type UserInfo = ReturnType<typeof getInfo>;
❗️ 이런 기법은 신중하게 사용해야 합니다. 적용 대상이 값인지 타입인지 정확히 알고, 구분해서 처리해야 합니다.
제너릭 타입은 타입을 위한 함수와 같습니다.
제너릭 타입에서 매개변수를 제한할 수 있는 방법은 extends를 사용하는 것입니다.
interface Name {
first: string;
last: string;
}
type User<T extends Name> = [T, T];
const couple1: User<Name> = [
{first: 'JS', last: 'Lee'},
{first: 'HS', last: 'G'}
]; // OK
const couple2: User<{first: string}> = [
{first: 'Sonny'},
{first: 'Cj'}
]; // 'Name' 타입에 필요한 'last' 속성이 '{first: string;}' 타입에 없습니다.
위에 나온 Pick 의 정의는 extends를 사용해서 완성할 수있습니다.
타입 체커를 통해 기존 예제를 실행해 보면 오류가 발생합니다.
type Pick<T, K> = {
[k in K]: T[k]
// ~ 'K' 타입은 'string | number | symbol' 타입에 할당할 수 없습니다.
}
// K는 실제로 T의 키의 부분 집합, 즉 keyof T가 되어야 합니다.
type Pick<T, K extends keyof T> = {
[k in K]: T[k]
}; // 정상
⭐️ 타입이 값의 집합이라는 관점에서 생각하면 extends를 '확장'이 아니라 '부분 집합' 이라는 걸 이해하면 됩니다.
타입스크립트에서는 타입에 '인덱스 시그니처'를 명시하여 유연하게 매핑을 표현할 수 잇습니다.
type Rocket = {[property: string]: string}; // 인덱스 시그니처
const rocket: Rocket = {
name: 'Lee 9',
variant: 'v1.0',
thrust: '4,940 kN'
} // 정상
단점
interface Rocket {
name: string;
variant: string;
thrust_kN: number;
}
const falconHeavy: Rocket = {
name: 'Lee 27',
variant: 'v1',
thrust_kN: 4,940
} // 정상
❗️ 인덱스 시그니처는 동적 데이터를 표현할 때 사용합니다.
연관 배열(associative array)의 경우, 객체에 인덱스 시그니처를 사용하는 대신 Map 타입을 사용하는 것을 고려할 수 있습니다.
이는 프로토타입 체인과 관련된 유명한 문제를 우회합니다.
어떤 타입에 가능한 필드가 제한되어 있는 경우라면 인덱스 시그니처로 모델링하지 말아야 합니다.
=> 선택적 필드 또는 유니온 타입으로 모델링 대체.
interface Row1 { [column: string]: number } // 너무 광범위함
interface Row2 {a: number; b?: number; c?: number; d?: number} // 최선
type Row3 =
| { a: number; }
| { a: number; b: number; }
| { a: number; b: number; c: number; }
| { a: number; b: number; c: number; d: number}; // 가장 정확하지만 사용하기 번거로움
type Vec3D = Record<'x' | 'y' | 'z', number>;
// Type Vec3D = {
// x: number;
// y: number;
// z: number;
// }
type Vec3D = {[k in 'x' | 'y' | 'z']: number};
// Type Vec3D = {
// x: number;
// y: number;
// z: number;
//}
type ABC = {[k in 'a' | 'b' | 'c']: k extends 'b' ? string : number};
// Type ABC = {
// a: number;
// b: string;
// c: number;
//}
요약
📌 런타임 때까지 객체의 속성을 알 수 없을 경우에만 인덱스 시그니처를 사용합니다.
📌 안전한 접근을 위해 인덱스 시그니처의 값 타입에 undefined를 추가하는 것을 고려해야 합니다.
📌 가능하다면 인터페이스, Record, 매핑된 타입 같은 인덱스 시그니처보다 정확한 타입을 사용하는 것이 좋습니다.