| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
- 리액트
- 알고리즘
- 입문
- styled-components
- 비전공자
- useRef
- MegabyteSchool
- 프론트엔드
- JavaScript
- 프로그래머스
- 모던 딥 다이브 자바스크립트
- 개발 공부
- 모던 자바스크립트 딥 다이브
- 국비지원교육
- next.js
- 코딩테스트
- 패스트캠퍼스
- Github
- 이벤트
- 메가바이트스쿨
- CSS
- 내일배움카드
- TypeScript
- react
- 개발자취업부트캠프
- 공식문서
- useMemo
- GIT
- 자바스크립트
- 자료구조
- Today
- Total
개발 기록 남기기✍️
[TypeScript] Object.keys()의 타입 좁히기 본문
🚨 마주친 문제
Record<'a' | 'b', string> 형태의 객체를 만들어서 Object.keys() 또는 Object.entries()를 실행했을 때 key의 타입은 ('a' | 'b')[] 형태가 아닌 string[]이 됩니다.
이 때문에 다음과 같이 Object.keys()로 추출한 객체의 키로 객체에 접근하려고 할 때 에러가 발생하게 됩니다.
type Person = {
name: string, age: number, id: number,
}
declare const me: Person;
Object.keys(me).forEach(key => {
// 🚨 Element implicitly has an ‘any’ type because expression of type ‘string’ can’t be used to index type ‘Person’. No index signature with a parameter of type ‘string’ was found on type ‘Person’
console.log(me[key])
})
🤨 왜 이런 문제가 발생하는 걸까요?
타입스크립트에서는 Object.keys()의 인터페이스를 다음과 같이 지정하고 있습니다.
// typescript/lib/lib.es5.d.ts
interface Object {
keys(o: object): string[];
}
근데 다음과 같이 지정하면 우리가 원하는 타입대로 쓸 수 있는거 아니에요?🤔
interface Object {
keys<T extends object>(o: T): (keyof T)[];
}
타입스크립트에서 Object.keys를 이렇게 정의하지 않은데에는 구조적 타입 시스템과 관련이 있습니다.
🔎 타입스크립트의 구조적 타이핑
구조적 타이핑의 가장 기본적인 특성은 값을 할당할 때 정의된 타입에 필요한 속성을 가지고 있다면 호환된다는 점입니다.
따라서 타입 A가 B의 슈퍼셋인 경우(A는 B의 모든 프로퍼티를 포함) 타입 A를 B에 할당할 수 있습니다.
type Person = {
name: string
};
let person: Person;
const worker = {
name: 'Seonju',
job: 'Developer',
};
person = employee; // OK
worker는 Person 타입에 필요한 name 속성을 가지고 있기 때문에 그 외의 속성이 있더라도 person의 값으로 할당할 수 있습니다.
따라서 Object.keys의 타입을 좁힐 경우 다음과 같은 문제를 만날 수 있습니다.
type AB = {
a: string;
b: string;
}
function testFunction(arg: AB) {
return Object.keys(arg) as (keyof AB)[];
}
const argument = {
a: 'some',
b: 'thing',
c: 'unexpected',
};
const keys = testFunction(argument);

argument는 TestType의 서브셋이기 때문에 testFunction에서 타입 에러가 발생하지 않습니다.
실제로는 ('a' | 'b' | 'c')[] 타입이 리턴되어야 하지만 (keyof AB)[] 가 리턴되는 것을 확인할 수 있습니다.
정리하면, 객체는 런타임에 더 많은 속성을 가질 수 있기 때문에 Object.keys()의 타입은 (keyof T)[]이 아닌 string[] 타입이 되어야 합니다.
따라서 Object.entries 를 사용하여 객체를 순회하거나, 객체에 다른 속성이 포함되지 않을 것을 확신할 수 있는 경우에 (keyof T)[]로 캐스팅하여 사용해야 함을 인지해야 합니다.
💥 Object.keys, Object.entries의 타입을 좁혀주는 유틸 함수 만들기
네~ 하지만 저는 readonly인 객체를 사용할 것이기 때문에 타입 캐스팅을 해줄 것입니다~ 🐒
Object.keys()의 리턴 타입을 객체에서 추출한 key 값으로 강제해주는 유틸 함수를 만들었습니다.
type Entries<T> = {
[K in keyof T]: [K, T[K]];
}[keyof T][];
type TypedObject = Record<string, unknown>;
const getTypedEntries = <T extends TypedObject>(obj: T) => {
return Object.entries(obj) as Entries<T>;
};
const getTypedKeys = <T extends TypedObject>(obj: T) => {
const typedKeys = Object.keys(obj) as Array<keyof T>;
return typedKeys;
};
export const ObjectTyped = {
entries: getTypedEntries,
keys: getTypedKeys,
};
해당 유틸 함수의 매개변수의 타입을 object로 지정할 경우, object는 interface, class의 상위 타입이기 때문에 모든 타입의 값들을 할당할 수 있게 됩니다.
따라서 TypedObject 커스텀 타입을 만들어줘 객체의 타입을 명확하게 지정해주고, Object.keys()의 리턴 타입은 Array<keyof T>로 타입 캐스팅 처리를 했습니다.
근데… 코드를 유심히 살펴보니 마음에 들지 않는 부분이 있네요🤨
T[] 형태의 타입은 빈 배열도 허용하기 때문이죠!
❓ zod란?
zod는 스키마 선언 및 유효성 검증 라이브러리입니다.
zod를 통해 런타임 단계에서의 타입 에러를 잡아줄 수 있습니다.
GitHub - colinhacks/zod: TypeScript-first schema validation with static type inference
TypeScript-first schema validation with static type inference - colinhacks/zod
github.com
zod의 경우에는 enum의 타입은 다음과 같이 정의되어있어요.
ZodEnum<T extends [string, ...string[]]>
따라서 TypedObject.keys()의 결과값을 z.enum에 전달하면 다음과 같은 에러가 발생해요.

zod의 enum에는 빈 배열을 전달할 수 없도록 되어있습니다.
이렇게 되면 우리의 아름다운 TypedObject.keys는 무용지물이 되어버려요.
object에 key가 존재하면 빈 배열이 아닌 값이 있는 배열 형태의 타입을 반환하도록 Array의 타입을 좁혀보겠습니다.
type TypedArray<T> = T[] extends [...T[]] ? [T, ...T[]] : T[];
const getTypedKeys = <T extends TypedObject>(obj: T) => {
const typedKeys = Object.keys(obj) as TypedArray<keyof T>;
return typedKeys;
};
이제 Object.keys()를 통해 반환된 키 배열은 해당 객체의 키 집합에 대한 안전한 타입을 보장받을 수 있게 되었습니다. 😁
'Front-End > TypeScript' 카테고리의 다른 글
| [TypeScript] TypeScript satisfies 연산자 (0) | 2024.04.29 |
|---|---|
| [TypeScript] 템플릿 리터럴 타입을 키로 갖는 객체 만들기 (1) | 2024.04.18 |
| 타입스크립트로 블록체인 만들기 (4) (0) | 2022.12.05 |
| 타입스크립트로 블록체인 만들기 (3) (2) | 2022.12.05 |
| 타입스크립트로 블록체인 만들기 (2) (0) | 2022.12.05 |