baidu / san

A fast, portable, flexible JavaScript component framework
https://baidu.github.io/san/
MIT License
4.72k stars 549 forks source link

3.11 的 index.d.ts 试用以及问题 #699

Closed 100pah closed 2 years ago

100pah commented 2 years ago

我所基于的版本是: 8764a60f4e2e7b1066345ab9194fb3ce1dcb8a55

测试用例:

如下测试用例中,对于有疑问的地方(即 ts 不满足需求或者报错的地方), 已用 [question n] 标出。 可后续以此作为讨论的引用。

import { defineComponent } from 'san/types/index-3.11.d';

///////////////////////////////////////////////////////
// Type Utils
export type Expect<T extends true> = T
export type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false
export type NotEqual<X, Y> = true extends Equal<X, Y> ? false : true
export type IsAny<T> = 0 extends (1 & T) ? true : false
export type NotAny<T> = true extends IsAny<T> ? false : true
export type IsExtends<T, K> = T extends K ? true : false
export type ExpectValidArgs<FUNC extends (...args: any[]) => any, ARGS extends any[]> = ARGS extends Parameters<FUNC>
  ? true
  : false

type UnknownObject = Record<string, unknown>;

/**
 * [question 1]
 * 实践中,在 this 上定义属性很多见。
 * 用这个帮助函数可以避免写 as unknown as Xxx
 * 并且强制指定类型(而非根据初始化参数隐式指定类型)
 * 比如:
 * ```ts
 * defineComponent({
 *
 *    // Case 1:
 *    // enum 可取值 'xxx' 'yyy', 默认值 'xxx'
 *    // 如果无此帮助函数,则容易被开发者写成:
 *    someEnum: 'xxx'
 *    // 但是实际应该写成:
 *    someEnum: 'xxx' as ('xxx' | 'yyy')
 *    // 有此帮助函数后,强制写成:
 *    someEnum: declareProp<'xxx' | 'yyy'>('xxx')
 *
 *    // Case 2:
 *    // 类型为 Product 初始值为 undefined
 *    // 如果无此帮助函数,需要写成:
 *    someObject: undefined as unknown as Product
 *    // 有此帮助函数后,可以写成
 *    someObject: declareProp<Product>()
 *
 *    fn1() {
 *        // 在这里要能推导出他们的类型。
 *        this.someEnum
 *        this.someObject
 *    }
 * });
 * ```
 *
 * 因此不知 san 是否默认提供这种类型支持,
 * 还是说留给 san 使用者自己决定。
 */
export function declareProp<TPropType>(initValue?: any) {
    return initValue as unknown as TPropType;
}

const Cmpt1 = defineComponent({

    // 向 this 上挂属性,并且初始值为 undefined 。
    myProp: declareProp<number>(),

    template: `...`,

    initData() {

        // [question 5] 
        // 建议:这里访问 myMethod2 和 myProp 是否要类型报错 ?
        // 运行时虽然这个 this 确实是 component 实例,
        // 但是执行到 initData 时候, this 上还啥都没挂。
        // 当然,这一点不是很确定,酌情。
        const num = this.myMethod2();
        const ppp = this.myProp;

        return {
            ooo: 7890,
            hahaha: null
        };
    },

    computed: {
        computedEE() {
            const num = this.myMethod2();
            const ppp = this.myProp;

            // [question 6]
            // hahaha 已经定义过了,此处不应返回 any 类型。
            const hahaha = this.data.get('hahaha');
            type typeCase6_1 = Expect<NotEqual<typeof hahaha, any>>;
            // undefinedProp 没有定义过,此处也不应返回 any 类型,建议 unknown
            const undefinedProp = this.data.get('undefinedProp');
            type typeCase6_2 = Expect<NotEqual<typeof undefinedProp, any>>;
            // data 也不应是 any 类型。
            const data = this.data.get();
            type typeCase6_3 = Expect<NotEqual<typeof data, any>>;
            // 事实上,这里 get 总是返回 any 类型。
            // 而函数返回值,感觉不应是 any , explicit any 会传播,在使用者意料之外。
            // 同 question 4

            // [question 7] 
            // 这里调用 set 应该类型报错为好,确实此处 this 上没有 set 方法。
            this.data.set('hahaha', 6789);
        },
        computedCC() {
        }
    },

    myMethod1(): void {
        // 正确: 可以使用自定义属性。
        this.myProp = 123;

        console.log(this.template);
        // 正确: notDefinedProp 报错。
        console.log(this.notDefinedProp);

        // 可以使用 san component 上 built-in 方法。
        this.fire('asdf', {
            zxcv: 123
        });

        // 可以调用自定义方法。
        const num = this.myMethod2();

        // [question 3] 
        // hahaha 不应是 any 类型。
        const hahaha = this.data.get('hahaha');
        type typeCase3_1 = Expect<NotEqual<typeof hahaha, any>>;
        const data = this.data.get();
        // [question 2] data 中访问 hahah 会报错。
        // hahaha 已经声明了,应该能推导出。
        console.log(data.hahaha);
        console.log(data);
        // [question 10] undefinedProp 不应是 any ,建议是 unknown 。
        const undefinedProp = this.data.get('undefinedProp');
        type typeCase10_1 = Expect<NotEqual<typeof undefinedProp, any>>;

        this.data.set('hahaha', 6789);

        this.on('lll', event => {
            // 正确: 这里是 implicit any ,提示开者这里没有推导出类型。
            console.log(event);
        });

        console.log(this.el);

        // [question 12] 
        // value 不应该 explicit any ,建议是 unknown 或者 implicit any 。
        this.watch('hahaha', value => {
            type typeCase12_1 = Expect<NotEqual<typeof value, any>>;
            console.log(value);
        });

        const el1 = document.createElement('div');
        this.attach(document.body, el1);
    },

    myMethod2(): number {
        return 123;
    }

});

const Cmpt2 = defineComponent({

    // 向 this 上挂属性,并且初始值为 undefined 。
    myProp: declareProp<number>(),

    // san component 的模版定义,可选。
    template: `...`,

    // san component 的初始化 data,可选。
    data: {
        hahaha: 1234
    },

    myMethod1(): void {
        // 正确: 可以使用自定义属性。
        this.myProp = 123;

        // 正确: 可以使用 san component 上 built-in 方法。
        this.fire('asdf', {});

        // 正确: 可以调用自定义方法。
        this.myMethod2();

        // 正确: 可以使用 san component 上 built-in 的 data 方法。
        const hahaha = this.data.get('hahaha');
        const data = this.data.get();
        console.log(hahaha);
        console.log(data);
        this.data.set('hahaha', 6789);

    },

    myMethod2(): void {
    }

});

const cmpt1 = new Cmpt1();
const notDefinedDataProp1 = cmpt1.data.get('notDefinedDataProp1');
// [question 4] 
// notDefinedDataProp1 不应是 any 。
// explicity any 会导致传播,使得在开发人员无感知的情况下,类型推导失效不报错,
// 开发人员以为类型是安全的,实际不安全。
// 此处应该是 unknwon 为好。
type typeCase4_1 = Expect<NotEqual<typeof notDefinedDataProp1, any>>;

const cmpt2 = new Cmpt2();
const notDefinedDataProp2 = cmpt2.data.get('notDefinedDataProp2');
type typeCase4_2 = Expect<NotEqual<typeof notDefinedDataProp2, any>>;

//////////////////////////////////////////////////////////////////////////
// BaseComponent
// [question 8] 继承问题
// san.defineComponent 返回 component 类型是符合事实的。
// 但是实践中还有一种写法( swan 中广泛使用,历史原因),即继承,亦或理解为 mixin 也可:
//
// file: base.ts
export const myBaseConfig = {
    baseFn1(): number {
        return 1;
    },
    baseFn2(x: string): string {
        return 'a';
    }
};
// -----------------------
// file: sub1.ts
export const mySub1Config = {
    subFn1(): void {
        // 在 sub 里访问 base 的 method
        // 比如 base 里定义了 dispatchEvent 等特殊公用方法。
        // 这时,类型推导中,怎么能得到这个 baseFn2 的类型定义呢?
        const str = this.baseFn2();

    },

    // swan 中使用的是一种方式:
    // 用这个特殊属性,把 base component 的 type ,声明给 sub component config 。
    // 从而就能 sub 里使用 base 的方法。
    // 或者,这叫做 mixin 也行,这里可以传多个 base component type ,
    // 如 __tsBaseConfig: declareProp<Base1 & Base2>(),
    __tsBaseConfig: declareProp<BaseComponentType>(),
    // 这种方式未必最美,不知有没有什么别的更好的方式。
    // 而 san 自带的类型定义,是否支持这种场景?
    // 还是说, san 不去支持, swan 自己支持?
};
export const mySubMergedConfig = merge(mySub1Config, myBaseConfig);
// -----------------------

function merge(sub: unknown, base: unknown): unknown {
    return null;
}
errorrik commented 2 years ago

@100pah 提的点不少,我先简单回一下。

关于 [question 8] ,这个不在支持范围。如果是各种 object 去 merge,那自己实现 function merge<T1, T2>(a: T1, b:T2): T1&T2。无论如何,在 define 时,应该是明确的。

关于 [question 4],这是个选择的问题。在 能推导就推导不能推导就放过 和 推导不出来也非让你声明 之间,框架选择了前者。这个问题,和 @otakustay 讨论过。

其他问题,我觉得很对,也是考虑到和支持的。可能和目前 swan 场景中使用方式不同。具体用法,我写完例子放出来。

100pah commented 2 years ago

这是个选择的问题。在 能推导就推导不能推导就放过 和 推导不出来也非让你声明 之间,框架选择了前者。

这一点我还没有理解:如果返回 any

  1. 感觉是否可能不是 “不能推导就放过这个不能推导的地方”,而是 “不能推导则让所有直接或间接依赖此处的地方的推导都失效”。
  2. 这种 “自动放过” 是使用者无法感知的,使用者无法知道哪里经过了类型校验哪里没经过校验,于是使用者也无法信赖校验。
  3. “返回 any 导致类型校验失效但是以为校验有效”,印象里 echarts 里出过问题(但是 sorry 我忘了具体例子 😓)

Edit: 在 vue 中尝试:

Screen Shot 2021-11-24 at 1 57 45 AM

当推导不出时,返回的是 any ,但是同时也会提示推导不出,从而这 “推导不出” 是开发者可感知的,这种方式感觉也是可以。但“开发者不可感知地返回 any” 感觉不像是好方式 🤔 (不知我的理解和实验是否正确)

otakustay commented 2 years ago

Vue这属于推导出来了的。推导不出来的情况更类似于React的useCallbackgetDerivedStateFromError的参数

如果放unknown,就是强制使用者每次都写as Xxx做类型声明,而用any则是选择性做类型声明

这里的核心问题是通过类型推导并不是明确地知道“这是个不存在的属性”(不然就直接上never了),解决的办法应该是再试试能不能优化这个推导,直接改成unknown对用户的负担太大了

100pah commented 2 years ago

解决的办法应该是再试试能不能优化这个推导

感觉是~

而用any则是选择性做类型声明

就是不知道,开发者,怎么能知道,“这里是 any ,没有推导出来”。

otakustay commented 2 years ago

就是不知道,开发者,怎么能知道,“这里是 any ,没有推导出来”。

要我说的话,.以后自动提示没出来就是any了哈哈哈

errorrik commented 2 years ago

@100pah https://github.com/baidu/san/tree/master/example/todos-ts

花大半天写了个简单的 example proj,并尽可能使用各种不同的方式去写组件。由于 3.11 还没有发布,所以试不了。想体验开发时候的感觉,可以通过如下步骤:

  1. 删除 package.json 里的 san 3.11.0 依赖
  2. npm i
  3. 到 node_modules 目录下 ln -s ../../.. san

好了,下面是对开发时支持的一些考虑的说明:

关于 computed ,这里之前考虑到了。但确实实现的有问题,已经修复。 @100pah 很细心!

组件,有两种声明方式:extends Component 和 defineComponent。

关于组件的数据

对于 view = f(data) 的组件体系,数据是核心,开发者在声明组件之前,应该把数据定义出来,无论采用什么方式声明组件。然后,开发时就能获得 data 的类型支持(在 get、initData、computed内、set等地方) https://github.com/baidu/san/blob/master/example/todos-ts/src/todo/list.ts#L46 https://github.com/baidu/san/blob/master/example/todos-ts/src/ui/category-picker.ts#L13

关于组件的方法

关于对偷懒开发者的容忍

当然,如果你实在很懒,连数据都不想定义,也是可以的。这是个理念,不知道有没有不认同的。 https://github.com/baidu/san/blob/master/example/todos-ts/src/todo/form.ts#L49

san 的 d.ts 在 Component 和 defineComponent 的 T 提供了空的默认值,可以让开发者偷懒。当然,偷懒就没法获得 data 相关的支持。 https://github.com/baidu/san/blob/master/types/index.d.ts#L90 https://github.com/baidu/san/blob/master/types/index.d.ts#L552

errorrik commented 2 years ago

3.11 已经发布,例子 https://github.com/baidu/san/tree/master/example/todos-ts 也可以直接用了。

看起来暂时没问题,我就先关了。有新的问题再开新 issue 讨论吧