Search code examples
javascripttypescriptapi-designentity-component-system

Typescript omit getters setters readOnly for Partial data?


how i can omit getters,setters,methods in Partial<> data ?

Or existe a way to get this kind of architecture with typescript ?

I need a types with full access, and a types without readOnly.

I try make a small example here, see note://

export const $A = (() => {
    class A {
        _uid?: string = 'generateUID()';
        _title?: string = '';
        children?: A[] = [];
        B?: InstanceType<typeof $B['B']> = $B.create();
        get Title() {
            return this._title || this._uid;
        }
        constructor(data: Partial<A> = {}) {
            Object.assign(this, data);
        }
        getFullTitle() {
            return this._title + this._uid;
        }
    }
    function create(data?: Partial<A>) {
        return new A(data);
    }
    function renderer(A: A) {
        //#1 Allow full access public A in this renderer entity
        const { Title, _title, _uid } = A; // acces all props autorized here
        const fulltitle = A.getFullTitle(); // acces methods autorized here
        const childTitle = A.children[0]?.Title; // acces child props autorized here
    }

    return {
        create,
        renderer,
        A,
    };
})();
export const $B = (() => {
    class B {
        _foo?: string = '';
        get Foo() {
            return this._foo + 'foo';
        }
        constructor(data: Partial<B> = {}) {
            Object.assign(this, data);
        }
    }
    function create(data?: Partial<B>) {
        return new B(data);
    }
    function renderer(B: B) {}

    return {
        create,
        renderer,
        B,
    };
})();

// API
//#2 Partial props only (omit methods,getter,setter) for a higth level API !
$A.create({ Title: '' }); // should not visible and lint error !
$A.create({ getFullTitle: () => '' }); // should not visible and lint error !
// want allow only this structure for higth API
$A.create({
    _title: '',
    _uid: '',
    B: $B.create(),
    children: [$A.create({ _title: '', _uid: '' }), $A.create({ _title: '', _uid: '' })],
});

What i want is inside function renderer(A: A) { able to use all public props methodes,getter,setters. And inside function create(data?: Partial<A>) allow only partial datas (without getters,setters,methodes).

So i want a trick to for get only yellow props here, when i use create() enter image description here

I try many scenario without success, Always hit a wall somewhere ! if you have a another's way to structure this ? thanks


Solution

  • To my knowledge, you cannot detect getters or setters in the type system; see microsoft/TypeScript#10196 for more information. It is said there that

    Interfaces make no distinction between regular properties and accessor properties--it's an implementation detail left up to the implementer of the interface.


    If you want to detect properties whose values are function-typed, you can use KeysMatching from the answer to this question:

    type KeysMatching<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];
    
    type MethodKeysOfA = KeysMatching<A, Function>;
    // type MethodKeysOfA = "getFullTitle"
    

    If you want to detect properties marked as readonly (and getter-only properties count), you can do so with ReadonlyKeys from the answer to this question:

    type IfEquals<X, Y, A = X, B = never> =
        (<T>() => T extends X ? 1 : 2) extends
        (<T>() => T extends Y ? 1 : 2) ? A : B;
    type ReadonlyKeys<T> = {
        [P in keyof T]-?: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, never, P>
    
    type ReadonlyKeysOfA = ReadonlyKeys<A>
    // type ReadonlyKeysOfA = "Title"
    

    You could then combine these to produce your "partial" A:

    type MyPartialA = Omit<A, MethodKeysOfA | ReadonlyKeysOfA>
    /* type MyPartialA = {
         _uid?: string | undefined;
         _title?: string | undefined;
        children?: A[] | undefined;
        B?: B | undefined;
    } */
    

    If you use MyPartialA instead of Partial<A> for the type of the data parameter in create(), you get the behavior you're looking for:

    $A.create({ Title: '' }); // error!
    $A.create({ getFullTitle: () => '' }); // error!
    
    $A.create({
        _title: '',
        _uid: '',
        B: $B.create(),
        children: [$A.create({ _title: '', _uid: '' }), $A.create({ _title: '', _uid: '' })],
    }); // okay
    

    It's also possible that you want to think of MyPartialA as the set of optional properties from A. That is, instead of excluding methods and readonly properties, you include things which might not be present in A. If so, you could use the OptionalKeys type from the answer to the same question with ReadonlyKeys:

    type OptionalKeys<T> = { [K in keyof T]-?:
        ({} extends { [P in K]: T[K] } ? K : never)
    }[keyof T];
    
    type OptionalKeysOfA = OptionalKeys<A>;
    // type OptionalKeysOfA = "_uid" | "_title" | "children" | "B"
    

    You can see that this leads to the same MyPartialA as before:

    type MyPartialA = Pick<A, OptionalKeysOfA>;
    /* type MyPartialA = {
       _uid?: string | undefined;
       _title?: string | undefined;
       children?: A[] | undefined;
       B?: B | undefined;
    } */
    

    Playground link to code