ascoders/weekly

看到最近在讨论ts, 之前遇到了一个奇怪的问题,抛出来大家一起看看

Kntt opened this issue · 6 comments

Kntt commented

目标

实现一个 get方法, 功能 和 lodash里面的get一样
get(obj, key)
实现get的类型推导能力

  1. 根据obj, 可以推导出key的类型
  2. 推导出返回值的类型

遇到个奇怪的问题,

  • 单独实现的get方法, 推导返回值类型总是出错, 如果基于class 实现个实例方法, get推导就一切正常
    代码如下:
const data = {
    name: 'Luis',
    age: 18,
    tel: 123456789,
    address: {
        city: 'beijing',
        street: 'xiamen'
    },
    school: {
        name: 'beijing',
        tel: 123456789,
        hasFee: true,
        address: {
            city: 'shanghai',
            street: 'nightmen'
        },
        teacher: [1, 2, 3]
    }
}
export class MyC {
    data = {
        name: 'Luis',
        age: 18,
        tel: 123456789,
        address: {
            city: 'beijing',
            street: 'xiamen'
        },
        school: {
            name: 'beijing',
            tel: 123456789,
            hasFee: true,
            address: {
                city: 'shanghai',
                street: 'nightmen'
            },
            teacher: [1, 2, 3]
        }
    } 
    //  @ts-ignore
    get<P extends ObjectPropName<MyC['data']>>(path: P): ObjectPropType<MyC['data'], P> {
        // @ts-ignore
        return (path as string).split('.').reduce((a, b) => a[b], this.data);
    }
}

type ObjectPropName<T, Path extends string = ''> = {
    [K in keyof T]: K extends string
    ? T[K] extends Record<string, any>
        ? ObjectPath<Path, K> | ObjectPropName<T[K], ObjectPath<Path, K>>
        : ObjectPath<Path, K>
    : K;
}[keyof T];

type ObjectPath<Pre extends string, Curr extends string> = `${Pre extends '' ? Curr: `${Pre}.${Curr}`}`;

type ObjectPropType<T, Path extends string> = 
    Path extends keyof T
    ? T[Path]
    : Path extends `${infer K}.${infer R}`
        ? K extends keyof T
            ? ObjectPropType<T[K], R>
        : unknown
    : unknown;

const c = new MyC();

// class 的原型方法, 可以使用 ObjectPropType 推导出返回值类型
const val = c.get('school.teacher');

type keys = ObjectPropName<typeof c.data>

// 定义单独方法  使用 ObjectPropType 推导返回值类型 都是 unknown
//  @ts-ignore
const get = <O extends Record<string, any>, P extends ObjectPropName<O>>(o: O, path: P): ObjectPropType<O, P> => {
    // @ts-ignore
    return (path as string).split('.').reduce((a, b) => a[b], o);
}

// 单独使用 ObjectPropType 也可以推导出类型
type valueType = ObjectPropType<typeof c.data, 'school.teacher'>

// 这里的 v 推导出 unknown
const v = get(data, 'school.address');

@ts-ignore 越多,则结果非预期概率越大

// 这里的 v 推导出 unknown
const v = get(data, 'school.address' as const);
/*
const v: {
    city: string;
    street: string;
}
*/

因为 ObjectPropType<typeof c.data, 'school.teacher'> 中 泛型 Path 的类型是 ’school.teacher‘

而 get(data, 'school.address') 对应到 ObjectPropType<T, Path> 中泛型 Path 的类型是 string

加上const 就会推导成字符串字面类型

Kntt commented
// 这里的 v 推导出 unknown
const v = get(data, 'school.address' as const);
/*
const v: {
    city: string;
    street: string;
}
*/

因为 ObjectPropType<typeof c.data, 'school.teacher'> 中 泛型 Path 的类型是 ’school.teacher‘

而 get(data, 'school.address') 对应到 ObjectPropType<T, Path> 中泛型 Path 的类型是 string

加上const 就会推导成字符串字面类型

@netwjx 感谢解惑~

uinz commented

贴一个我的实现

https://tsplay.dev/WKyDKN

type JoinDot<T extends string[], R extends string = ""> = T extends [
  infer U extends string,
  ...infer V extends string[]
]
  ? JoinDot<V, R extends "" ? U : `${R}.${U}`>
  : Exclude<R, "">;

export type ToPath<T, P extends string[] = []> = T extends unknown[]
  ? JoinDot<P> | ToPath<T[number], [...P, `${number}`]>
  : T extends object
  ?
      | JoinDot<P>
      | {
          [K in keyof T]-?: ToPath<T[K], [...P, K & string]>;
        }[keyof T]
  : JoinDot<P>;

export type Get<T, P> = P extends ""
  ? T
  : P extends keyof T
  ? T[P]
  : T extends unknown[]
  ? P extends `${number}`
    ? T[number]
    : P extends `${infer L extends number}.${infer R}`
    ? Get<T[L], R>
    : never
  : P extends `${infer L}.${infer R}`
  ? L extends keyof T
    ? Get<T[L], R>
    : undefined
  : undefined;
Kntt commented

刚刚研究了一下:
如果

type Obj = {
    a: {
        b: {
            c: { d: number }[]
        }
    },
    e: {
        f: string
    },
    g: number
    f: {
        [key: string]: any
    }
}

如果如上的类型, 会有死循环的提示 Type instantiation is excessively deep and possibly infinite.

uinz commented

刚刚研究了一下:

如果

type Obj = {

    a: {

        b: {

            c: { d: number }[]

        }

    },

    e: {

        f: string

    },

    g: number

    f: {

        [key: string]: any

    }

}

如果如上的类型, 会有死循环的提示 Type instantiation is excessively deep and possibly infinite.

可以做一个深度检测,一般实际使用情况,不超过10层就好。