Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generating declarations for readonly/shallowReadonly ref causes TS4058 error #4701 #3

Open
cuixiaorui opened this issue Dec 12, 2021 · 5 comments

Comments

@cuixiaorui
Copy link
Owner

cuixiaorui commented Dec 12, 2021

Generating declarations for readonly/shallowReadonly ref causes TS4058 error #4701

为什么要读他

可以学到什么

todo

开始时间

2021-12-13

12月第三周

@cuixiaorui cuixiaorui changed the title fix(types): ensure that DeepReadonly handles Ref type properly (fix #4701) #4714 Generating declarations for readonly/shallowReadonly ref causes TS4058 error #4701 Dec 12, 2021
@likui628
Copy link

likui628 commented Dec 12, 2021

版本

3.2.19

重现

  1. 点击gitpod工程
  2. ctrl + ~打开终端
  3. npm install安装依赖
  4. npm run build
> vue-refsymbol-issue@1.0.0 build
> tsc -p tsconfig.json

index.ts:3:17 - error TS4058: Return type of exported function has or is using name 'RefSymbol' from external module "/workspace/vue-refsymbol-issue/node_modules/@vue/reactivity/dist/reactivity" but cannot be named.

3 export function useCounter () {
                  ~~~~~~~~~~


Found 1 error.

原因

以下是出问题的代码,

import { ref, readonly } from 'vue'

export function useCounter () {
  const count = ref(0)

  function increment () {
    count.value++
  }

  return {
    count: readonly(count),
    increment
  }
}

问题就出在返回的count: readonly(count),count的类型被解析为RefSymbol,但是RefSymbol为私有的symbol

image

那么由此可以推测问题出在readonly函数,ctrl+鼠标左键查看readonly定义

/**
 * Creates a readonly copy of the original object. Note the returned copy is not
 * made reactive, but `readonly` can be called on an already reactive object.
 */
export declare function readonly<T extends object>(target: T): DeepReadonly<UnwrapNestedRefs<T>>;

因此我们需要再去看DeepReadonly定义

type Primitive = string | number | boolean | bigint | symbol | undefined | null
type Builtin = Primitive | Function | Date | Error | RegExp
export type DeepReadonly<T> = T extends Builtin
  ? T
  : T extends Map<infer K, infer V>
  ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
  : T extends ReadonlyMap<infer K, infer V>
  ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
  : T extends WeakMap<infer K, infer V>
  ? WeakMap<DeepReadonly<K>, DeepReadonly<V>>
  : T extends Set<infer U>
  ? ReadonlySet<DeepReadonly<U>>
  : T extends ReadonlySet<infer U>
  ? ReadonlySet<DeepReadonly<U>>
  : T extends WeakSet<infer U>
  ? WeakSet<DeepReadonly<U>>
  : T extends Promise<infer U>
  ? Promise<DeepReadonly<U>>
  : T extends {}
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : Readonly<T>

于是我们可以尝试去重现这个问题TS Playground

type Primitive = string | number | boolean | bigint | symbol | undefined | null
type Builtin = Primitive | Function | Date | Error | RegExp
export type DeepReadonly<T> = T extends Builtin
  ? T
  : T extends {}
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : Readonly<T>

declare const RefSymbol: unique symbol

export interface Ref<T = any> {
  value: T
  [RefSymbol]: true
}

/*
type test = {
  readonly value: number;
  readonly [RefSymbol]: true;
}
 */
type test = DeepReadonly<Ref<number>>

因此可以判定问题就出在DeepReadonly没有特殊处理Ref类型,错误的将RefSymbol暴露出来。

解决

修复只需要增加Ref类型的判断TS Playground

type Primitive = string | number | boolean | bigint | symbol | undefined | null
type Builtin = Primitive | Function | Date | Error | RegExp
export type DeepReadonly<T> = T extends Builtin
  ? T
  : T extends Ref<infer U>
  //增加Ref类型判断
  ? Ref<DeepReadonly<U>>
  : T extends {}
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : Readonly<T>

declare const RefSymbol: unique symbol

export interface Ref<T = any> {
  value: T
  [RefSymbol]: true
}

/*
type test = Ref<number>
 */
type test = DeepReadonly<Ref<number>>

@emwanwei163
Copy link

emwanwei163 commented Dec 12, 2021

由于在fix issue 1111的 PR里面引入了 [RefSymbol]

image

DeepReadonly把 这个 [RefSymbol]也标记为 readonly了,说明对Ref类型的对象也进行了递归readonly处理。
出错是因为[RefSymbol]没有被导出。declare const RefSymbol: unique symbol (在这个declare const 前面加export可能就不报错了,但是不符合作者的目的)

fix是加入对Ref类型的判断

image

收获1 unique symbol的使用

image
看历史记录,作者在这里是有几次不同的改动的。
使用过 const isRefSymbol = Symbol()
意图是用isRefSymbol来区别Ref对象和恰好带value的普通对象。但是对任意对象检查symbol比检查普通property慢很多,所以还是加入了普通property __v_isRef: true
然后isRefSymbol就删了
接着在commit b772bba5587726e78b20ccb9b61374120bd4b0ae 引入DeepReadonly
后来呢,又把这个revert了,Symbol改为了unique symbol. 然后 __v_isRef 被封装到了RefImpl里面。

class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  public readonly __v_isRef = true

这个就是和issue1111相关联的, 达到的效果是 [RefSymbol] 在 d.ts里面可见, 但是IDE打点点不出来,而且这个unique symbol只用作标记类型用,对象并不会有这个值。 但是对_shallow属性由于注释里面写@internal,d.ts看不到,但是对象里面存在。 (只让用户看见和自动补齐value)

编译后

export declare interface Ref<T = any> {
    value: T;
    /**
     * Type differentiator only.
     * We need this to be in public d.ts but don't want it to show up in IDE
     * autocomplete, so we use a private Symbol instead.
     */
    [RefSymbol]: true;
    /* Excluded from this release type: _shallow */
}

可以对比一下有没有[RefSymbol]的效果
有的[RefSymbol]的情况

export interface Ref<T = any> {
  value: T
  [RefSymbol]: true
}

type Fake = { value: number }

type TestRef<T> = T extends Ref<infer U> ? U : T;
type Result = TestRef<Fake>  // 此处判断正确  type Result = { value: number }

没有的情况

export interface Ref<T = any> {
  value: T
}

type Fake = { value: number }

type TestRef<T> = T extends Ref<infer U> ? U : T;
type Result = TestRef<Fake>  // 此处判断错误  type Result = number, 无法区分假的带value的类型

收获2 这个DeepReadonly 足够全面的写法

对DeepReadonly的实现再复习一下思路。

  1. 对ReadOnly的实现梳理一下记忆
    比如有个utiltiy type的作用是产生和输入类型一样
    type Same<T> = { [P in keyof T] : T[P] }
    然后在每个属性上面加readonly, 这就是系统提供的基本ReadOnly
    type ReadOnly<T> = { readonly [P in keyof T] : T[P] }

  2. 但是要对子类型递归的处理就需要DeepReadonly
    直观的想法就是对 T[P] 也都加上 readonly
    type DeepReadonly<T> = { readonly [P in keyof T] : DeepReadonly<T[P]> } // 但是这个没有条件判断, 递归不起来
    那么加上条件判断就是
    type DeepReadonly<T> = { readonly [P in keyof T] : T[P] 是基本类型吗 ? T[P] : DeepReadonly<T[P]> }

    于是就有关于T[P] 是否为基本类型的判断方式, 比如 T[P] extends string | number | Function | Array<infer _> 这样的
    type DeepReadonly<T> = { readonly [P in keyof T] : T[P] extends string | number | Function | Array<infer _> ? T[P] : DeepReadonly<T[P]> }
    为了递归看起来简洁,就把判断基本类型放前面
    type DeepReadonly<T> = T extends string | number | Function | Array<infer _> ? T : { readonly [P in keyof T] : DeepReadonly<T[P]> }
    或者有人有更好的写法
    type DeepReadonly<T> = keyof T extends never ? T : { readonly [P in keyof T] : DeepReadonly<T[P]> }

  3. vue3里面的写法更严谨全面
    不仅判断基本类型, 还判断内建类型

type Primitive = string | number | boolean | bigint | symbol | undefined | null
type Builtin = Primitive | Function | Date | Error | RegExp

而且对Map, ReadOnlyMap, WeakMap, Set, ReadOnlySet, WeakSet都进行了判断和处理
虽然这是一个类似带很多if else的类型判断,感觉逻辑很清晰, 好像有个处理的图表

- Map<infer K, infer V> ---------> ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>        
- ReadOnlyMap< .. > -------------> ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> 
- WeakMap<infer K, infer V> -----> WeakMap<DeepReadonly<K>, DeepReadonly<V>>                 

- Set<infer U> -------------------> ReadonlySet<DeepReadonly<U>>
- ReadonlySet<infer U> -----------> ReadonlySet<DeepReadonly<U>>
- WeakSet<infer U> ---------------> WeakSet<DeepReadonly<U>>

另外再加Promise和Ref, 然后对{}就是普通的递归了

@shuzong
Copy link

shuzong commented Dec 13, 2021

版本:3.2.19

产生issue实例:

import { ref, readonly } from 'vue'

export function useCounter() {
  const count = ref(0)

  function increment () {
    count.value++
  }

  return {
    count: readonly(count),
    increment
  }
}

ide和build时报错:index.ts:3:17 - error TS4058: Return type of exported function has or is using name 'RefSymbol' from external module "vue-refsymbol-issue-master/node_modules/@vue/reactivity/dist/reactivity" but cannot be named.

首先解读RefSymbol,这是什么?作者代码里没有提到RefSymbol,那我可不可以试着看函数返回对象类型定义

function useCounter(): {
    count: {
        readonly value: number;
        readonly [RefSymbol]: true;
    };
    increment: () => void;
}

有发现了,这里返回的count是个对象,拥有只读value和[RefSymbol]属性,那此时我觉得我应该看看count声明是ref到底做了什么?

export declare interface Ref<T = any> {
    value: T;
    /**
     * Type differentiator only.
     * We need this to be in public d.ts but don't want it to show up in IDE
     * autocomplete, so we use a private Symbol instead.
     */
    [RefSymbol]: true;
    /* Excluded from this release type: _shallow */
}

找到[RefSymbol]了,是ref生成时定义的接口类型,那我看看ref初始化方法吧

export function ref(value?: unknown) {
  return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

看到这里,我看出ref对象拥有value和[RefSymbol]属性,那为什么readonly后就没有该属性了? 是不是readonly出了问题,一起看看readonly吧

export declare function readonly<T extends object>(target: T): DeepReadonly<UnwrapNestedRefs<T>>;
DeepReadonly
export declare type DeepReadonly<T> = T extends Builtin 
   ? T : T extends Map<infer K, infer V> 
   ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> : T extends ReadonlyMap<infer K, infer V> 
   ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> : T extends WeakMap<infer K, infer V> 
   ? WeakMap<DeepReadonly<K>, DeepReadonly<V>> : T extends Set<infer U> 
   ? ReadonlySet<DeepReadonly<U>> : T extends ReadonlySet<infer U> 
   ? ReadonlySet<DeepReadonly<U>> : T extends WeakSet<infer U> 
   ? WeakSet<DeepReadonly<U>> : T extends Promise<infer U> 
   ? Promise<DeepReadonly<U>> : T extends {} 
   ? {
    readonly [K in keyof T]: DeepReadonly<T[K]>;
} : Readonly<T>;

DeepReadonly缺少对ref类型的处理,试着根据其他声明类型把ref加上

? Ref<DeepReadonly<U>> : T extends Ref<infer U>

尝试npm run build试一下

嗯,可以了,IDE也不报错了

此bug尝试解除,源码其实并没有看的特别懂,还是需要一步步查和扩展思路,通过这个bug和之前的reactive and ref type infer is Wrong #1111 #2,让我对ref对象创建和生成更加了解

ps:看源码时遇到了一行不太理解,不会通过jest去查,
声明ref(0)后在,在ref对象初始化时

function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

可知rawValue为0

export function isRef(r: any): r is Ref {
  return Boolean(r && r.__v_isRef === true)
}
r为0的情况下,r && r.__v_isRef === true,这句话应该不成立的

没有时间继续深入看了 记录一下

@jp-liu
Copy link

jp-liu commented Dec 14, 2021

@shuzong 如果是 Ref 那么他就是一个 RefImpl 类, 不是 0 去做比较

@shuzong
Copy link

shuzong commented Dec 14, 2021

@shuzong如果的英文Ref那么他就是一个RefImpl类,不是0去做比较

十分感谢,昨晚我也看了一下,参数并不是int类型的0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants