타입스크립트의 Object.keys 타입 추론
타입스크립트를 사용하다가 이해할수 없는 에러와 마주쳤다.
const obj = { key1:"", key2:"" };
Object.keys(obj).forEach((section) => {
delete obj[section];
}
위와 같은 코드가 있을때
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ key1: string; key2: string; }'.
No index signature with a parameter of type 'string' was found on type '{ key1: string; key2: string; }'.ts(7053)
string은 obj의 index(key)가 아니라는 에러 인데 언뜻 이해가 되지 않는다. obj의 key를 순회했는데 obj의 key가 아니라니?
멍청해보이는 타입스크립트가 이렇게 동작하는 이유를 알아보자.
Object.keys 타입
// typescript/lib/lib.es5.d.ts
interface Object {
/**
* Returns the names of the enumerable string properties and methods of an object.
* @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
*/
keys(o: object): string[];
}
타입스크립트에서 Object.keys 는 위처럼 무조건 string 배열을 반환하게 되어 있다.
언뜻보면 Object를 제너릭으로 받을수 있게 하면 될것 같은데 이해가 되지 않는다.
타입스크립트의 구조적 타이핑
타입스크립트는 프로퍼티가 누락되었거나 잘못된 타입일 때 에러를 표시하지만 추가 프로퍼티가 포함되어 있어도 에러를 표시하지 않는다.
function saveUser(user: { name: string; age: number }) {}
const user = { name: 'Alex', age: 25, city: 'Reykjavík' };
saveUser(user); // 타입 에러가 아님
이는 의도된 동작이다. 타입스크립트에서 타입은 슈퍼셋이라면 할당이 가능하다.
type Human = {
name: string;
age: number;
gender: string;
};
type Woman = {
name: string;
age: number;
gender : "female";
}
const human: Human = {name:"",age:0,gender:""};
const woman : Woman ={name:"",age:0,gender:"female"};
const human2 : Human = woman
const woman2 : Woman = human // 타입 에러
Woman은 Human에 할당이 가능하지만 반대로 Human은 Woman에 할당이 불가능하다.
여기서 Human타입은 Woman타입의 슈퍼셋( 타입을 포함하는 상위 타입 )이다.
이건 상호 포함관계에 있으니깐 이해가 쉽다.
여기서 헷갈리는건 프로퍼티가 추가로 있는 경우 이다.
type Human = {
name: string;
age: number;
gender: string;
};
type Woman = {
name: string;
age: number;
}
const human: Human = {name:"",age:0,gender:""};
const woman : Woman ={name:"",age:0};
const human2 : Human = woman //타입에러
const woman2 : Woman = human
Human 타입에는 gender라는 프로퍼티가 필요하다고 에러가 나지만 반대의 경우는 할당이 가능하다.
타입으로 지정된 프로퍼티가 없을때는 에러가 나지만 지정되지 않은 프로퍼티가 추가로 있는 것은 괜찮다.
Object.keys의 안전하지 않은 사용
타입스크립트에는 이런 구조적 타이핑의 특성이 있을때 Object.keys를 사용하면 어떻게 될까.
type User = {
name : string;
age : number;
}
const validate = {
name : (name:string) => {
if(name.length > 10){
return false;
}
return true;
},
age : (age:number) => {
if(age > 100){
return false;
}
return true;
}
}
function validator(user:User){
Object.keys(user).forEach((key) => {
const result = validate[key](user[key]); // 타입 에러
if(!result){
return false;
}
})
}
const user = {name:"kim", age: 10, city:"seoul"};
validator(user); // 타입 에러가 아님
const user = {name:"kim", age: 10, city:"seoul"};를 보면 User에 없는 city프로퍼티가 있음에도 function validator에 인자로 전달이 가능하다.
이 상태에서 Object.keys로 순회하면 validate.city는 undefined이기 때문에 undefined is not function 에러가 런타입에 발생할것이다.
이런식으로 동작하는 것에 어떤 장점이 있을까 확인해보자.
구조적 타이핑 활용하기
구조적 타이핑은 많은 유연성을 제공한다.
function getKeyboardShortcut(e: KeyboardEvent) {
if (e.key === 's' && e.metaKey) {
return 'save';
}
if (e.key === 'o' && e.metaKey) {
return 'open';
}
return null;
}
expect(getKeyboardShortcut({ key: 's', metaKey: true })).toEqual('save');
expect(getKeyboardShortcut({ key: 'o', metaKey: true })).toEqual('open');
expect(getKeyboardShortcut({ key: 's', metaKey: false })).toEqual(null);
37개의 프로퍼티를 모두 선언할수는 없다.
interface KeyboardShortcutEvent {
key: string;
metaKey: boolean;
}
function getKeyboardShortcut(e: KeyboardShortcutEvent) {}
하지만 이런식으로 필요한 프러퍼티만 선언하도록 할 수 있다.
KeyboardEvent 또한 슈퍼셋이기 때문에 문제 없이 할당 할 수 있다. 또한 KeyboardEvent에 종속되지 않아 함수의 활용더 훨씬 유용해졌다.
'개발관련 > 자바스크립트 팁' 카테고리의 다른 글
javascript fetch response body가 null인 경우 처리 하는 방법. (0) | 2024.03.07 |
---|---|
자바스크립트의 setTimeout은 왜 정확한 타이밍을 보장하지 못할까 (0) | 2023.08.03 |
자바스크립트 parseInt / parseFloat / number를 이용하지 않고 문자열을 숫자로 형변환 하는 구현 방법 (0) | 2023.07.11 |
자바스크립트를 예로 들어서 의존성 역전 원칙(Dependency Inversion Principle, DIP) (0) | 2023.07.10 |
자바스크립트의 실행 컨텍스트, Lexical Environment 및 관련 컨셉 이해하기 (0) | 2023.05.18 |