Skip to content

Commit 2f4a012

Browse files
committed
docs(object-type): 구체적인 타입 예제 추가
1 parent cad0abe commit 2f4a012

File tree

6 files changed

+108
-51
lines changed

6 files changed

+108
-51
lines changed

_posts/js/object-type/docs.mdx

Lines changed: 69 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ description: '객체 타입의 이해와 문자열 리터럴 확장하기'
44
tags: ['typescript', 'object', 'type']
55
coverImage: 'cover.webp'
66
date: '2025-01-19T10:45:40.928Z'
7+
updatedAt: '2025-02-07T16:51:42.899Z'
78
---
89

910
![cover](cover.webp 'cover')
@@ -49,69 +50,103 @@ type StringHint = 'foo' | 'bar' | (string & {}) // foo, bar 힌트가 보이면
4950
5051
## string & \{}
5152
52-
`string & {}``string``{}`의 교집합이다. <u>`{}`는 객체의 최상위 타입이므로(후술)</u>, `string`과 교집합을 구하면 결국 `string` 타입과 동일하게 된다.
53+
```ts
54+
type Test = 'foo' | 'bar' | string extends string ? true : false; // true
55+
type Test = 'foo' | 'bar' | (string & {}) extends string ? true : false; // true
56+
```
57+
58+
`string & {}``string``{}`의 교집합이다.
59+
60+
<u>`{}`는 대부분의 타입을 포함하는 타입이므로(후술)</u>, `string`과 교집합을 구하면 결국 `string` 타입과 동일하게 동작하게 된다.
5361

54-
그런데 문자열 리터럴(`"foo", "bar"`)은 `string`의 서브셋이지 `string & {}`의 서브셋이 아니다(!!).
62+
그런데 문자열 리터럴(`"foo", "bar"`)은 `string`의 서브셋이지 `string & {}`의 서브셋이 아니다(!).
5563

56-
즉, 타입스크립트 컴파일러는 `string & {}` <u>구체적인 타입으로 간주</u>하여 문자열 리터럴(`"foo", "bar"`)과 `string & {}``string`으로 합치지 않고 구분하여 유니온 타입으로 표현하게 된다.
64+
`string & {}` <u>구체적인 타입</u>`string`으로 좁혀지지 않았기 때문에 타입스크립트 컴파일러는 해당 타입을 문자열 리터럴과 다른 타입으로 간주하게 된다.
5765

5866
이러한 특성 때문에 우리는 문자열 리터럴을 사용하면서 문자열을 확장할 수 있게 된다.
5967

6068
> 관련 내용은 타입스크립트 이슈([Literal String Union Autocomplete #29729](https://github.com/microsoft/TypeScript/issues/29729#issuecomment-567871939))에서 확인할 수 있다.
6169
62-
6370
### 구체적인 타입
6471

65-
타입스크립트에서는 타입을 "구체적으로 나타내는" 방식이 있다. 이는 타입스크립트의 타입 시스템이 [구조적 서브 타이핑](/posts/typescript-subtyping#%EA%B5%AC%EC%A1%B0%EC%A0%81-%EC%84%9C%EB%B8%8C-%ED%83%80%EC%9D%B4%ED%95%91%EC%9D%B4%EB%9E%80)을 따르기 때문이다.
72+
타입스크립트의 타입 시스템은 [구조적 서브 타이핑](/posts/typescript-subtyping#%EA%B5%AC%EC%A1%B0%EC%A0%81-%EC%84%9C%EB%B8%8C-%ED%83%80%EC%9D%B4%ED%95%91%EC%9D%B4%EB%9E%80)따른다. 그렇기에 각 타입을 구조적으로 비교한다.
6673

67-
타입스크립트는 타입을 구조적으로 비교한다. 이때 교집합(`&`)을 사용하면 더 구체적인 타입으로 간주하게 된다.
74+
이때 교집합(`&`)을 사용하면 더 구체적인 타입으로 나타낼 수 있다.
6875

6976
```ts
7077
type A = { a: string };
7178
type B = { a: 'A' | 'B'; b: number };
72-
type C = A & B; // { a: 'A' | 'B', b: number };
79+
type ANB = A & B; // { a: 'A' | 'B', b: number };
80+
```
81+
82+
구조적 서브 타이핑에 익숙하지 않으면 위의 예에서 "중첩"만 생각해 중첩되는 `'a'` 프로퍼티만 가져와 `A & B = { a: 'A' | 'B' }`로 생각할 수 있다. 하지만 <u>`A & B`의 의미는 `A``B` 타입 모두를 만족하는 타입</u><ExternalAnchor href="https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#intersection-types" />을 의미한다.
83+
84+
집합적 관점에서 생각해보자. `x ∈ A ∩ B`일 필요충분조건은 `x ∈ A` 혹은 `x ∈ B` 이다<ExternalAnchor href="https://ko.wikipedia.org/wiki/%EA%B5%90%EC%A7%91%ED%95%A9" />.
85+
86+
> \{★, ●}, \{★, ●, ◆}의 교집합은 \{★, ●}이다. 교집합은 부모와 같아질 수 있다.
87+
88+
```ts
89+
type Test1 = ANB extends A ? true : false; // true
90+
type Test2 = ANB extends B ? true : false; // true
7391
```
7492

75-
구조적 서브 타이핑에 익숙하지 않으면 위의 예에서 "교집합"만 생각해 중첩되는 `'a'` 프로퍼티만 가져와 `A & B = { a: 'A' | 'B' }`로 생각할 수 있다. 하지만 <u>`A & B`의 의미는 `A``B`의 모든 프로퍼티를 만족하는 타입</u>을 의미한다.
93+
그렇다면 위의 논리로 `ANB`를 살펴보자. `ANB``A``B`의 구체적인 타입(교집합)이므로 `ANB``A` 혹은 `B`로 간주될 수 있어야 한다.
7694

77-
모든 타입, 프로퍼티를 만족한다는 것은 결국 가장 구체적인(작은) 타입을 만족한다는 것이다.
95+
`A & B` 타입이 `A` 타입이 되려면 `{ a: string }`을 만족해야 하며 `B` 타입이 되기 위해선 `{ a: 'A' | 'B', b: number }`를 만족해야 한다.
7896

79-
따라서 `A`타입의 `'a'` 프로퍼티(`string`)와 `B`타입의 `'a'` 프로퍼티(`'A' | 'B'`)를 만족하는 구체적인 타입인 문자열 리터럴 타입(`'A' | 'B'`)과 `B` 타입의 `'b'` 프로퍼티(`number`)를 가져오는 것이다.
97+
이를 구조적으로 표현하기 위해서 `A & B``A``B`의 모든 속성을 가져오고 속성이 겹친다면 가장 좁은 타입을 적용하게 된다.
8098

81-
아래 예제를 보면 명확해진다.
99+
> \{ a: string, b: number } 타입은 A는 만족하지만 B는 만족하지 않기 때문.
100+
101+
아래 예제를 보면 더 명확해진다.
82102

83103
```ts
84-
const a = { a: 'A' };
85-
const aa = { a: 'A', b: 1 };
104+
type A = { a: string };
105+
type B = { a: 'A' | 'B'; b: number };
106+
type ANB = A & B; // { a: 'A' | 'B', b: number };
107+
108+
const a = { a: 'Astring' };
109+
const aa = { a: 'Astring', b: 1 };
86110
// 타입 B의 a 프로퍼티가 'A' | 'B' 타입이므로 더 좁은 타입으로 강제 지정해줘야 함.
87111
const b = { a: 'B' as 'A' | 'B', b: 2 };
88112
const bb = { a: 'B' as 'A' | 'B', b: 2, c: 'ddd' };
89113

90114
const goA = (param: A) => console.log(param);
91115
const goB = (param: B) => console.log(param);
92-
const goC = (param: C) => console.log(param);
116+
const goANB = (param: ANB) => console.log(param);
93117

94118
goA(a); // OK
95119
goA(aa); // OK. 타입에 없는 b 프로퍼티가 추가되어도 a가 있으므로 "구조적으로" 상관없음.
96120

97121
goB(b); // OK
98122
goB(bb); // OK
99123

100-
goC(a); // Error. b 프로퍼티가 없음.
101-
goC(aa); // Error. a 프로퍼티의 타입이 구조적으로 맞지 않음(넓음).
102-
goC(b); // OK
103-
goC(bb); // OK. c 프로퍼티가 추가되어도 a, b가 있으므로 "구조적으로" 상관없음.
124+
goANB(a); // Error. b 프로퍼티가 없음.
125+
goANB(aa); // Error. a 프로퍼티의 타입이 구조적으로 맞지 않음(넓음).
126+
goANB(b); // OK
127+
goANB(bb); // OK. c 프로퍼티가 추가되어도 a, b가 있으므로 "구조적으로" 상관없음.
104128
```
105129

106-
구조적 서브 타이핑의 논리에 따라 `string & {}``string`을 의미하여도 `string`보다 더 구체적인 "타입"으로 정의된다.
130+
요구하는 타입의 구조만 만족한다면 그 외의 속성이 있어도 타입 에러가 발생하지 않는 것을 확인할 수 있다.
107131

108-
이는 `string`과는 다른 타입이므로 문자열 리터럴과 유니온 타입을 지정할 때 `string`으로 잡아먹히지 않게 된다.
132+
따라서 `ANB``A`의 모든 속성과 `B`의 모든 속성을 가져온 타입이 된다.
133+
134+
```ts
135+
type Test1 = 'foo' extends string ? true : false; // true
136+
type Test2 = 'foo' & string extends string ? true : false; // true
137+
type Test3 = 'foo' & {} extends string ? true : false; // true
138+
type Test4 = string & {} extends string ? true : false; // true
139+
```
140+
141+
위에서 다뤘던 문자열 리터럴 `'foo'``string`의 구체적인 타입이다. 그렇기 때문에 문자열 리터럴 `'foo'``string`으로 간주되어도 문제가 없다(더 넓은 타입).
142+
143+
이러한 구조적 서브 타이핑의 논리에 따라 `string & {}`는 실질적으로 `string`과 동일한 의미여도 `string`보다 더 구체적인 "타입"으로 정의된다.
109144

110-
관련 내용이 더 궁금하다면 [Type Narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html)을 참고.
145+
이는 `string`과는 다른 타입이므로 문자열 리터럴과 유니온 타입을 지정할 때 `string`으로 잡아먹히지 않게 된다.
111146

112147
## Object vs object vs \{}
113148

114-
앞에서 "`{}`객체의 최상위 타입"이라고 했다. 그렇다면 객체의 최상위 타입인 `Object`, `object`, `{}`는 어떤 차이가 있을까?
149+
앞에서 "`{}`대부분의 타입을 포함하는 타입"이라고 했다. 그렇다면 객체의 최상위 타입인 `Object`, `object`, `{}`는 어떤 차이가 있을까?
115150

116151
이를 자세히 알기 위해선 자바스크립트의 타입을 이해해야 한다.
117152

@@ -205,6 +240,8 @@ const obj7: object = []; // OK.
205240

206241
`object` 타입은 [원시 타입이 아닌 모든 값을 나타낸다](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#object-type).
207242

243+
> object 타입은 타입스크립트 2.2 버전에서 추가되었다.
244+
208245
위에서 살펴본 자바스크립트의 타입을 기준으로 보면 원시 타입이 아닌 모든 값은 곧 객체 타입이다. 따라서 `object` 타입은 원시 타입을 제외한 모든 객체 타입을 나타낸다.
209246

210247
### \{\} 타입
@@ -225,7 +262,7 @@ const obj7: {} = []; // OK.
225262

226263
> 타입스크립트 공식 FAQ - [#Primitives are \{}, and \{} Doesn't Mean object](https://github.com/microsoft/TypeScript/wiki/FAQ#primitives-are---and---doesnt-mean-object).
227264
>
228-
> 해당 FAQ를 보면 [원시 타입](/posts/implicit-coercion#%EB%93%A4%EC%96%B4%EA%B0%80%EA%B8%B0-%EC%A0%84%EC%97%90)`{}`이며 `{}`객체 타입만을 의미하지 않는다고 한다.
265+
> 해당 FAQ를 보면 [원시 타입](/posts/implicit-coercion#%EB%93%A4%EC%96%B4%EA%B0%80%EA%B8%B0-%EC%A0%84%EC%97%90)`{}`이며 `{}`object 타입을 의미하지 않는다고 한다.
229266
230267
```ts
231268
const obj1: {} = { a: 1 };
@@ -274,11 +311,19 @@ type AAA = 'A' | 'B' | (string & object);
274311

275312
각 타입을 비교하면서 객체에 대한 이해도가 높아질 수 있었다.
276313

314+
```ts
315+
type EmptyObject = Record<string, never>; // {} 대신 사용.
316+
```
317+
318+
실제로 객체 타입을 정의할 때에는 `Record` 타입과 같은 유틸리티 타입을 사용하는 것이 좋다. 이는 타입스크립트의 타입 추론을 돕고, 코드의 가독성을 높여준다.
319+
277320
이 글이 흥미로웠다면 [Object.keys()는 왜 string[] 타입일까?](/posts/typescript-subtyping)도 읽어보길 추천한다. 이 글은 타입스크립트의 구조적 서브 타이핑에 대한 이해를 돕는다.
278321

279322
## 참고
280323

281324
- https://stackoverflow.com/questions/18961203/any-vs-object
282325
- https://stackoverflow.com/questions/49464634/difference-between-object-and-object-in-typescript
283326
- [알랑말랑 암묵적 형변환 말랑말랑 이해하기](/posts/implicit-coercion)
284-
327+
- https://www.reddit.com/r/typescript/comments/1e61bla/demystifying_intersection_and_union_types_in/
328+
- https://ivov.dev/notes/typescript-and-set-theory
329+
- https://blog.hwahae.co.kr/all/tech/9954

src/features/post/information/InformationContainer.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import type { FunctionComponent } from 'react';
22
import type { PostType } from '~/posts/models';
3-
import { ProfileSection } from './ProfileSection';
43
import HashTag from '~/shared/components/HashTag';
5-
import { UpdatedDate } from './UpdatedDate';
4+
import { ProfileSection } from './ProfileSection';
65
import { PublishedDate } from './PublishedDate';
76

87
type Props = {
@@ -28,8 +27,7 @@ export const InformationContainer: FunctionComponent<Props> = ({
2827
))}
2928
</section>
3029
<section>
31-
<PublishedDate date={date} />
32-
{updatedAt && <UpdatedDate date={updatedAt} />}
30+
<PublishedDate date={date} updatedDate={updatedAt} />
3331
</section>
3432
</>
3533
);
Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
1-
import type { FunctionComponent } from 'react';
1+
import { type FunctionComponent } from 'react';
22

33
import DateFormatter from '../../shared/components/DateFormatter';
44

55
interface PublishedDateProps {
66
date: string;
7+
updatedDate?: string;
78
}
89

910
export const PublishedDate: FunctionComponent<PublishedDateProps> = ({
1011
date,
12+
updatedDate,
1113
}) => {
1214
return (
1315
<div className="inline text-date-gray">
14-
Published <DateFormatter type="iso" date={date} />
16+
<DateFormatter type="iso" date={date} before="Published" />
17+
{updatedDate && (
18+
<>
19+
<span className="mx-1">·</span>
20+
<DateFormatter type="iso" date={updatedDate} before="Updated" />
21+
</>
22+
)}
1523
</div>
1624
);
1725
};

src/features/post/information/UpdatedDate.tsx

Lines changed: 0 additions & 17 deletions
This file was deleted.

src/features/shared/components/DateFormatter.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,28 @@ type DateType = 'iso' | 'default';
77

88
interface DateFormatterProps {
99
date: Date | number | string;
10+
before?: string;
1011
type?: DateType;
1112
format?: string;
1213
className?: string;
1314
}
1415

1516
const DateFormatter: FunctionComponent<DateFormatterProps> = memo(
16-
({ date, type = 'default', format, className }) => {
17+
({ date, type = 'default', format, className, before }) => {
1718
const dateTime =
1819
type === 'iso' ? parseISO(String(date)) : formatDate(date, format);
1920

2021
return (
21-
<time className={className} dateTime={dateTime} suppressHydrationWarning>
22-
{dateTime}
23-
</time>
22+
<>
23+
{before && <span className="mr-1">{before}</span>}
24+
<time
25+
className={className}
26+
dateTime={dateTime}
27+
suppressHydrationWarning
28+
>
29+
{dateTime}
30+
</time>
31+
</>
2432
);
2533
},
2634
);

src/features/shared/components/MDXSharedComponents.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,22 @@ const ImageHorizonWrap: FunctionComponent<{
1717
);
1818
};
1919

20+
const ExternalAnchor: FunctionComponent<{ href: string }> = ({ href }) => {
21+
return (
22+
<a
23+
className="underline-highlight-fade relative top-[0.1rem] px-0.25"
24+
title={href}
25+
href={href}
26+
target="_blank"
27+
rel="noopener noreferrer"
28+
>
29+
30+
</a>
31+
);
32+
};
33+
2034
/** MDX 내에서 바로 사용하는 컴포넌트 */
2135
export const MDXSharedComponents = {
2236
ImageHorizonWrap,
37+
ExternalAnchor,
2338
};

0 commit comments

Comments
 (0)