본문 바로가기
IT/typescript

[typescript] fattenObject 함수 type 만들기

by 내일은교양왕 2024. 8. 21.

 

아래 함수에 대해 Type을 정의해보자

function flattenObject(obj: any, result: any = {}){
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
                flattenObject(obj[key], result);
            } else {
                result[key] = obj[key];
            }
        }
    }
    return result;
}

const a = {
    a: 1,
    b: {
        c: [1, 2, 3],
        d: {
            e: 3,
            f: 4
        }
    },
    g: 5
    f: [123]
}

const flatObj = flattenObject(a);
console.log(flatObj);

// Output:
// { 'a': 1, 'c': 2, 'e': 3, 'f': 4, 'g': 5, 'f': [123]}

 

 

방법

primitive와 배열을 한묶음, 그외 object를 한묶음으로 나눠 타입을 지정

 

primitive와 배열에 대한 타입

먼저 단순한 object로 만들어 보자

type SimpleObject<T> = {
	[K in keyof T]: T[K]
}

 

모든 key & value pair가 그대로 들어온다.

여기서 primitive와 배열만 가져와보자

 

시도 1

type FilterValues<T> = T extends object ? T extends unknown[] ? T : never : T

type SimpleObject<T> = {
	[K in keyof T]: FilterValues<T[K]>
}


//추론된 값을 보니 object일 때 never type으로 나오네 
type b = {
    a: 1;
    b: never;
    g: 5;
    f: [123]
}

 

value에서 filter를 하지 말고 key에서 filter를 해보자

 

시도 2

type FilterKeys<T, K> = 
	K extends keyof T 
    	? T[K] extends object 
        	? T[K] extends unknown[] 
            	? K : never
            : K
        : never

type SimpleObject<T> = {
	[K in FilterKeys<T, keyof T>]: T[K]
}

// 추론이 잘 되었다.
type b = {
    a: 1;
    g: 5;
    f: [123];
}

 

 

이제 object에 대해서 타입을 정의해보자

먼저 primitive와 배열을 제외하는 타입을 작성하자

type FilterNestedKeys<T, K> = K extends keyof T 
	? T[K] extends object 
    	? T[K] extends unknown[] 
        	? never : K 
        : never 
    : never

type NestedObject<T extends object> = {
    [K in FilterNestedKeys<T, keyof T>]: T[K]
}

type c = NestedObject<{
    a: 1,
    b: {
        c: [1, 2, 3],
        d: {
            e: 3,
            f: 4
        }
    },
    g: 5
    f: [123]
}>

 

 

그다음 flatten 하게 타입을 만들어 보자

b object가 날라간것을 확인할 수 있다.

type Values<T extends object> = T[keyof T]

type FilterNestedKeys<T, K> = K extends keyof T 
	? T[K] extends object 
    	? T[K] extends unknown[] 
        	? never : K 
        : never 
    : never

type NestedObject<T extends object> = {
    [K in FilterNestedKeys<T, keyof T>]: T[K]
}

type FlatNestedObject<T extends object> = Values<NestedObject<T>>

type c = FlatNestedObject<{
    a: 1,
    b: {
        c: [1, 2, 3],
        d: {
            e: 3,
            f: 4
        }
    },
    g: 5,
    f: [123],
    e: {h: 1}
}>

// 추론된 타입
type c = {
    c: [1, 2, 3];
    d: {
        e: 3;
        i: 4;
    };
} | {
    h: 1;
}

 

추론된 타입이 union으로 되어 있어서 intersection으로 변경해보자

type ToIntersection<T> = (
    T extends any 
        ? (_:T) => void 
        : never
    ) extends (_: infer S) => void 
        ? S
        : never

type Values<T extends object> = T[keyof T]

type FilterNestedKeys<T, K> = K extends keyof T 
	? T[K] extends object 
    	? T[K] extends unknown[] 
        	? never : K 
        : never 
    : never

type NestedObject<T extends object> = {
    [K in FilterNestedKeys<T, keyof T>]: T[K]
}

type FlatNestedObject<T extends object> = ToIntersection<Values<NestedObject<T>>>

type c = FlatNestedObject<{
    a: 1,
    b: {
        c: [1, 2, 3],
        d: {
            e: 3,
            f: 4
        }
    },
    g: 5,
    f: [123],
    e: {h: 1}
}>

// 추론된 타입
type c = {
    c: [1, 2, 3];
    d: {
        e: 3;
        i: 4;
    };
} & {
    h: 1;
}

 

ToIntersection 함수에서 굳이 T extends any 냐고 물어보는 이유는 distributive law를 적용하기 위해서다

만약, T가 number | string으로 들어왔다면, 해당 법칙으로 인하여 ( _ : number | string) => void( _ : number) => void | ( _ : string) => void로 변경된다.

 

이 때 S가 number와 string보다 작으면서 제일 큰 타입으로 설정된다.

number와 string보다 작으면서 제일큰 타입은 number & string이다. (현실엔 없겠지만...)

 

이젠 재귀타입을 만들어서 nested object를 flat 하게 만들어야 한다.

type Values<T extends object> = T[keyof T]

type FilterNestedKeys<T, K> = K extends keyof T 
	? T[K] extends object 
    	? T[K] extends unknown[] 
        	? never : K 
        : never 
    : never

type NestedObject<T> = {
    [K in FilterNestedKeys<T, keyof T>]: T[K]
}

type ToIntersection<T> = (
    T extends any 
        ? (_:T) => void 
        : never
    ) extends (_: infer S) => void 
        ? S
        : never

type LazyRecursiveFlat<T> = T extends object ? FlatObject<T> : never

type FlatNestedObject<T> = ToIntersection<LazyRecursiveFlat<Values<NestedObject<T>>>>

type c = FlatNestedObject<{
    a: 1,
    b: {
        c: [1, 2, 3],
        d: {
            e: 3,
            i: 4
        }
    },
    g: 5
    f: [123],
    e: {h: 1}
}>


// 추론된 타입
type c = {
    c: [1, 2, 3];
    e: 3;
    i: 4;
} & {
    h: 1;
}

 

 

그리고 아까 primitive와 합친 타입을 만들고 디버깅이 편하게 Roll 타입을 추가해서 넣어주자

typescript hack을 쓴것 같다고 한다. 난 잘 모름 ㅋㅋ

type Values<T extends object> = T[keyof T]

type FilterNestedKeys<T, K> = K extends keyof T 
	? T[K] extends object 
    	? T[K] extends unknown[] 
        	? never : K 
        : never 
    : never

type NestedObject<T> = {
    [K in FilterNestedKeys<T, keyof T>]: T[K]
}

type ToIntersection<T> = (
    T extends any 
        ? (_:T) => void 
        : never
    ) extends (_: infer S) => void 
        ? S
        : never

type LazyRecursiveFlat<T> = T extends object ? FlatObject<T> : never

type FlatNestedObject<T> = ToIntersection<LazyRecursiveFlat<Values<NestedObject<T>>>>

type FlatObject<T> =  SimpleObject<T> & FlatNestedObject<T>

type c = FlatObject<{
    a: 1,
    b: {
        c: [1, 2, 3],
        d: {
            e: 3,
            i: 4
        }
    },
    g: 5
    f: [123],
    e: {h: 1}
}>

// 추론된 타입
type c = SimpleObject<{
    a: 1;
    b: {
        c: [1, 2, 3];
        d: {
            e: 3;
            i: 4;
        };
    };
    g: 5;
    f: [123];
    e: {
        h: 1;
    };
}> & SimpleObject<{
    c: [1, 2, 3];
    d: {
        e: 3;
        i: 4;
    };
}> & SimpleObject<...> & SimpleObject<...>


// Roll Type 추가
type Roll<T> = {
    [K in keyof T]: T[K]
} & {}

type FlatObject<T> =  Roll<SimpleObject<T> & FlatNestedObject<T>>

type c = FlatObject<{
    a: 1,
    b: {
        c: [1, 2, 3],
        d: {
            e: 3,
            i: 4
        }
    },
    g: 5
    f: [123],
    e: {h: 1}
}>

// 추론된 타입
type c = {
    a: 1;
    g: 5;
    f: [123];
    c: [1, 2, 3];
    e: 3;
    i: 4;
    h: 1;
}

 

 

이제 함수에 타입을 선언하고 마무리하자

function flattenObject<T extends object>(obj: T, result: any = {}): FlatObject<T> {
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
                flattenObject(obj[key], result);
            } else {
                result[key] = obj[key];
            }
        }
    }
    return result;
}

const a = {
    a: 1,
    b: {
        c: [1, 2, 3],
        d: {
            e: 3,
            f: 4
        }
    },
    g: 5,
    h: { hh: 1}
}

const flatObj = flattenObject(a);
console.log(flatObj);

// 추론된 타입
const flatObj: {
    a: number;
    g: number;
    c: number[];
    e: number;
    f: number;
    hh: number;
}

 

 

https://d2.naver.com/helloworld/7472830

 

'IT > typescript' 카테고리의 다른 글

[typescript] AtLeastOneRequired  (0) 2024.12.16
[typescript] -? 의미  (0) 2024.12.05
[typescript] infer  (0) 2024.08.14
[typescript] 함수타입(Function Type) 인자형  (0) 2024.08.14
[typescript] subtype  (0) 2024.08.14