TypeScript에서 강력한 유형 프롬프트로 알림 트리 만들기

31353 단어 typescriptwebdev
거의 모든 최신 애플리케이션에서 알림 배지를 볼 수 있습니다. 배지가 있는 버튼을 클릭하는 동안 세분화 알림 배지가 나타납니다. 원래 알림 장소로 안내하는 경로와 같습니다. 따라서 트리 데이터 구조체를 사용하여 알림 구조체를 나타낼 수 있는 클래스를 만들 수 있습니다.

약 1년 반 전에 위에서 이야기한 패키지를 게시했습니다. red-manager . 그러나 몇 가지 단점이 있습니다.
  • 경로에 대한 유형 프롬프트가 없습니다.
  • 소스 코드가 너무 복잡합니다.

  • 이러한 문제의 원인은 자바스크립트 파일에서 마이그레이션되었기 때문입니다.

    이제 더 강력하게 만들기 위해 문자열 리터럴 유니온 유형을 사용하여 유형 프롬프트를 강화하고 동시에 코드를 더 간단하게 만듭니다.

    먼저 어떻게 보이는지 봅시다:

    // define the root node at first.
    const root = NotificationTree.root(['notification', 'me']);
    




    나머지 코드.

    const notification = root.expand('notification', ['comments', 'likes', 'something']);
    notification.getChild('likes').setValue(20);
    const something = notification.getChild('something');
    something.setValue(10);
    root.dump();
    

    dump 함수는 콘솔에서 트리를 인쇄할 수 있습니다. 다음과 같습니다.



    이 클래스의 일반 정의는 다음과 같습니다.

    class NotificationTree<Prev extends string = string, Curr extends string = string> {
        // ...
    }
    

    Prev는 조상 경로를 나타내고 Curr는 자식 이름을 나타냅니다. 아마도 이 설명은 그리 직관적이지 않을 것입니다. root 의 유형 서명에 대해 살펴보겠습니다.

    const root: NotificationTree<"@ROOT", "notification" | "me">
    


    루트 노드로서 첫 번째 일반"@ROOT"은 해당 이름입니다. 두 번째 일반: "notification" | "me"는 모든 하위 이름을 포함하는 공용체 유형입니다.

    루트 노드를 정의한 후 expand 함수를 사용하여 "트리를 성장"시킵니다. IntelliSense는 첫 번째 매개 변수를 입력하는 동안 확장할 수 있는 모든 분기 이름을 제공하고 제한합니다. 우리는 오타를 두려워하지 않을 것입니다 :)

    const notification = root.expand('notification', ['comments', 'likes', 'something']);
    


    두 번째 매개변수는 "알림"하위 트리의 모든 하위 이름을 나타내는 문자열 배열입니다. 타입 시그니쳐를 보자.

    const notification: NotificationTree<"@ROOT/notification", "comments" | "likes" | "something">
    


    루트의 시그니쳐와 비슷해 보이지만 차이점은 첫 번째 제네릭 타입이 템플릿 문자열이고 값이 조상에서 자기 자신으로의 경로라는 점입니다.

    전체 코드:

    This file is written in wepo-project/web-client, if you are interested, welcome to check it out!



    const ROOT_NAME = "@ROOT";
    type SPLIT = "/";
    type JoinType<A extends string, B extends string> = `${A}${SPLIT}${B}`
    type Callback = (val: number) => void;
    
    export type ExtractType<Type> = Type extends NotificationTree<infer P, infer C> ? NotificationTree<P, C> : never;
    export type ExtractPrev<Type> = Type extends NotificationTree<infer P, string> ? P : never;
    export type ExtractCurr<Type> = Type extends NotificationTree<string, infer C> ? C : never;
    
    /**
     * NotificationTree
     * The `ROOT` instance can only be instantiated once.
     * Prev: ancestors path
     * Curr: children names
     */
    export class NotificationTree<Prev extends string = string, Curr extends string = string> {
        static ROOT: NotificationTree | null = null;
    
        name: string;
        private parent: NotificationTree | null;
        private children: Record<Curr, NotificationTree>;
        private _value: number = 0;
        private listener: Map<Callback, { target: any, once: boolean }>;
    
        /**
         * Create an root node of NotificationTree
         * also can create by constructor
         * @returns Root Node
         */
        static root<Name extends string = string>(children: Name[]): NotificationTree<typeof ROOT_NAME, Name> {
            if (this.ROOT) {
                console.error(`can not create root node duplicatly`)
                return this.ROOT
            } else {
                const _root = new NotificationTree(null, ROOT_NAME, children);
                this.ROOT = _root;
                return _root;
            }
        }
    
        private constructor(parent: NotificationTree<string, string> | null, name: Prev, childrenName: Curr[]) {
            this.parent = parent;
            this.name = name;
            this.expandChildren(childrenName);
            this.listener = new Map();
        }
    
        /**
         * append children with name list
         * @param childrenName 
         */
        private expandChildren(childrenName: Curr[]) {
            const children: typeof this.children = {} as any;
            for (const name of childrenName) {
                if (children[name] === void 0) {
                    children[name] = new NotificationTree(this as any, name, []);
                } else {
                    console.warn(`duplicate node name: ${name}`);
                }
            }
            this.children = children;
        }
    
        get value() {
            return this._value;
        }
    
        /**
         * use private decorations to prevent external calls to value setters
         */
        private set value(newVal: number) {
            const delta = newVal - this._value;
            if (this.parent) {
                this.parent!.value += delta;
            }
            this._value = newVal;
            try {
                for (const [callback, { target, once }] of this.listener.entries()) {
                    callback.call(target, newVal);
                    if (once) {
                        this.unListen(callback);
                    }
                }
            } catch (e) {
                // use try-catch to prevent break the setter chain
                console.error(e);
            }
        }
    
        /**
         * append children to this node with specify name list
         * @param which 
         * @param names 
         * @returns 
         */
        expand<Name extends string, WhichOne extends Curr>(which: WhichOne, names: Name[]): NotificationTree<JoinType<Prev, WhichOne>, Name> {
            this.children[which].expandChildren(names);
            return this.children[which];
        }
    
        /**
         * set value, it will trigger ancestors' changed their value.
         * make sure it's leaf node otherwise value will out of sync.
         * @param value 
         */
        setValue(value: number) {
            if (Object.keys(this.children).length) {
                console.warn(`this node has children, set it's value can't keep the values consistent`)
            }
            this.value = value;
        }
    
        /**
         * get children by name.
         * make children field private in order to prevent children modified external.
         * to prevent 
         * @param childName 
         * @returns 
         */
        getChild<N extends Curr, C extends string>(childName: N): Omit<NotificationTree<JoinType<Prev, N>, C>, 'getChild'> {
            if (childName in this.children) {
                return this.children[childName]
            } else {
                throw new Error(`${childName} is not [${this.name}]'s child`);
            }
        }
    
        /**
         * subscribe value changed event
         * @param callback 
         * @param options target: context, once: cancel once triggered
         * @returns a handler to unsubscribe event
         */
        listen(callback: Callback, options: {
            target?: any,
            once: boolean,
        } = { target: null, once: false }) {
            this.listener.set(callback, { target: options.target, once: options.once });
            return { cancel: () => this.unListen(callback) }
        }
    
        /**
         * unsubscribe value changed event
         * @param callback
         */
        unListen(callback: Callback) {
            this.listener.delete(callback);
        }
    
        /**
         * dump value to console to show value intuitively in console
         */
        dump() {
            console.groupCollapsed(`${this.name} -> ${this.value}`);
            for (const key in this.children) {
                const child = this.children[key];
                if (Object.keys(child.children).length) {
                    child.dump();
                } else {
                    console.log(`${child.name} -> ${child.value}`)
                }
            }
            console.groupEnd();
        }
    }
    

    좋은 웹페이지 즐겨찾기