首页 > 解决方案 > 如何拥有一个显式类型参数和一个推断类型参数?

问题描述

我正在使用一个 UI 库,该库具有用于构建类似于此的表的 API:

type Column<Record> = {
    keys: string | Array<string>;
    render: (prop: any, record: Record) => React.ReactNode;
}

render 函数的第一个参数将由库提供,方法是 basic column.render(record[column.keys], record)。如果column.keys是一个数组,它会被解释为记录下的“路径”,大约是这样的record[keys[0]][keys[1]]...[keys[keys.length - 1]]:下面的示例略有改动,因此它使用Pick<...>算法代替,只是为了有一个更简单但仍然有效的示例。

// Our record type
interface Entity {
    a: string;
    b: number;
    c: boolean;
}

// helper type, does the following:
// GetOrPickProps<Entity, 'a'> -> Entity['a']
// GetOrPickProps<Entity, ['b', 'c']> -> Pick<Entity, 'a' | 'c'>
type GetOrPickProps<E, K extends (keyof E | Array<keyof E>)> = K extends keyof E
    ? E[K]
    : K extends Array<infer K2>
        ? Pick<E, K2 & keyof E>
        : never;

// My first attempt at a Column type
type Column<E, K extends (keyof E | Array<keyof E>)> = {
    keys: K;
    render: (prop: GetOrPickProps<E, K>) => string;
}

// ...but it doesn't work
const columns: Array<Column<Entity, /* What goes here??? */>> = [
    {
        keys: 'a',
        render: a => a,
    },
    {
        keys: ['a', 'c'],
        render: ({ a, c }) => c ? a : 'something else',
    }
]

如果我明确地将'a' | ['a', 'c']第二个参数设置为Column,则两个渲染函数的类型都是(prop: Entity['a'] | Pick<Entity, 'a' | 'c'>) => string.

如果我将第二个参数设为Column可选(可能像K extends ... = unknown) typescript 实际上不会再推断类型,而是显式unknown使用prop.

我怎样才能有一个类型来推断它的部分道具以约束其他道具,但也接受显式类型参数?

TS-游乐场在这里

标签: typescripttypescript-generics

解决方案


请让我知道它是否适合您:

interface Entity {
    a: string;
    b: number;
    c: boolean;
}

type Column<Key, Arg> = {
    keys: Key,
    render: (arg: Arg) => string
}

type Keys = 'a' | ['a', 'c'];


// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
    k: infer I
) => void
    ? I
    : never;

// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type UnionToOvlds<U> = UnionToIntersection<
    U extends any ? (f: U) => void : never
>;

// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type PopUnion<U> = UnionToOvlds<U> extends (a: infer A) => void ? A : never;

// credits https://stackoverflow.com/users/125734/titian-cernicova-dragomir
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;

type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
    ? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
    : [T, ...A];


type ReducerElem = string;

type Reducer<
    Arr extends ReadonlyArray<ReducerElem>,
    Result extends Record<string, any> = {}
    > = Arr extends []
    ? Result
    : Arr extends [infer H]
    ? H extends ReducerElem
    ? Result & Record<H, H>
    : never
    : Arr extends readonly [infer H, ...infer Tail]
    ? Tail extends ReadonlyArray<ReducerElem>
    ? H extends ReducerElem
    ? Reducer<Tail, Result & Record<H, H>>
    : never
    : never
    : never;

type MapPredicate<T> =
    T extends string
    ? Column<T, T>
    : T extends Array<string>
    ? Column<T, Reducer<T>>
    : never

type Mapped<
    Arr extends Array<unknown>,
    Result extends Array<unknown> = []
    > = Arr extends []
    ? []
    : Arr extends [infer H]
    ? [...Result, MapPredicate<H>]
    : Arr extends [infer Head, ...infer Tail]
    ? Mapped<[...Tail], [...Result, MapPredicate<Head>]>
    : Readonly<Result>;

type Result = Mapped<UnionToArray<'a' | ['a', 'c']>>;


const columns: Result = [
    {
        keys: 'a',
        render: a => a,
    },
    {
        keys: ['a', 'c'],
        render: ({ a, c }) => c ? a : 'something else',
    }
]

如果您同意此解决方案,我将提供更多解释。

除此之外,herehere,在我的博客中,您可以找到一些解释

操场

这是Reducer实用程序类型的 js 类似物

const reducer = (arr: ReadonlyArray<Elem>, result: Record<string, any> = {}): Record<string, any> => {
    if (arr.length === 0) {
        return result
    }

    const [head, ...tail] = arr;

    return reducer(tail, { ...result, [head]: 'string' })
}

更新

您可以使用辅助功能,但有一个缺点。您仍然必须提供显式类型。好消息 - 如果不允许显式类型,TS 会抱怨。


interface Data {
    a: string;
    b: number;
    c: boolean;
}

type Column<Key, Arg> = {
    keys: Key,
    render: <T extends Arg>(arg: T) => string
}


// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
    k: infer I
) => void
    ? I
    : never;

// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type UnionToOvlds<U> = UnionToIntersection<
    U extends any ? (f: U) => void : never
>;

// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type PopUnion<U> = UnionToOvlds<U> extends (a: infer A) => void ? A : never;

// credits https://stackoverflow.com/users/125734/titian-cernicova-dragomir
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;

type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
    ? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
    : [T, ...A];


type ReducerElem = string;

type Predicate<T> = T extends keyof Data ? Record<T, Data[T]> : never

type Reducer<
    Arr extends ReadonlyArray<ReducerElem>,
    Result extends Record<string, any> = {}
    > = Arr extends []
    ? Result
    : Arr extends [infer H]
    ? H extends ReducerElem
    ? Result & Predicate<H>
    : never
    : Arr extends readonly [infer H, ...infer Tail]
    ? Tail extends ReadonlyArray<ReducerElem>
    ? H extends ReducerElem
    ? Reducer<Tail, Result & Predicate<H>>
    : never
    : never
    : never;

type MapPredicate<T> =
    T extends string
    ? Column<T, T>
    : T extends Array<keyof Data>
    ? Column<T, Reducer<T>>
    : never

type Mapped<
    Arr extends Array<unknown>,
    Result extends Array<unknown> = []
    > = Arr extends []
    ? []
    : Arr extends [infer H]
    ? [...Result, MapPredicate<H>]
    : Arr extends [infer Head, ...infer Tail]
    ? Mapped<[...Tail], [...Result, MapPredicate<Head>]>
    : Readonly<Result>;


type Format<Keys extends ReadonlyArray<keyof Data>> =
    UnionToArray<Keys[number]> extends ReadonlyArray<string>
    ? Reducer<UnionToArray<Keys[number]>>
    : never


interface Union<Keys extends keyof Data> {
    keys: Keys,
    render: (a: Data[Keys]) => void
}

interface WithArray<Keys extends ReadonlyArray<keyof Data>> {
    keys: Keys,
    render: (a: Format<Keys>) => void
}


type Base<Keys> = Keys extends Array<keyof Data> ? WithArray<Keys> : Keys extends keyof Data ? Union<Keys> : any

const builder = <V extends Array<keyof Data>, U extends keyof Data>(columns: [...(Base<[...V]> | Base<U>)[]]) => columns

const result = builder([{
    keys: ['c'], render: (prop: Record<"c", boolean>) => prop
}, {
    keys: 'b', render: (prop) => prop
}])

操场

您还可以合并所有可能的值。就像我在这里做的一样。但是如果你的Entity界面包含超过 5 个道具,它就不起作用了。

或者,您可以为每个实体使用 helper:


interface Data {
    a: string;
    b: number;
    c: boolean;
}

type Column<Key, Arg> = {
    keys: Key,
    render: <T extends Arg>(arg: T) => string
}


// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
    k: infer I
) => void
    ? I
    : never;

// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type UnionToOvlds<U> = UnionToIntersection<
    U extends any ? (f: U) => void : never
>;

// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type PopUnion<U> = UnionToOvlds<U> extends (a: infer A) => void ? A : never;

// credits https://stackoverflow.com/users/125734/titian-cernicova-dragomir
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;

type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
    ? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
    : [T, ...A];


type ReducerElem = string;

type Predicate<T> = T extends keyof Data ? Record<T, Data[T]> : never

type Reducer<
    Arr extends ReadonlyArray<ReducerElem>,
    Result extends Record<string, any> = {}
    > = Arr extends []
    ? Result
    : Arr extends [infer H]
    ? H extends ReducerElem
    ? Result & Predicate<H>
    : never
    : Arr extends readonly [infer H, ...infer Tail]
    ? Tail extends ReadonlyArray<ReducerElem>
    ? H extends ReducerElem
    ? Reducer<Tail, Result & Predicate<H>>
    : never
    : never
    : never;

type MapPredicate<T> =
    T extends string
    ? Column<T, T>
    : T extends Array<keyof Data>
    ? Column<T, Reducer<T>>
    : never

type Mapped<
    Arr extends Array<unknown>,
    Result extends Array<unknown> = []
    > = Arr extends []
    ? []
    : Arr extends [infer H]
    ? [...Result, MapPredicate<H>]
    : Arr extends [infer Head, ...infer Tail]
    ? Mapped<[...Tail], [...Result, MapPredicate<Head>]>
    : Readonly<Result>;


type Format<Keys extends ReadonlyArray<keyof Data>> =
    UnionToArray<Keys[number]> extends ReadonlyArray<string>
    ? Reducer<UnionToArray<Keys[number]>>
    : never

function maker<K extends Array<keyof Data>>(keys: K, cb: (a: Format<K>) => void): { keys: K, render: (a: Format<K>) => void }
function maker<K extends keyof Data>(keys: K, cb: (a: Data[K]) => void): { keys: K, render: (a: Data[K]) => void }
function maker(keys: keyof Data | Array<keyof Data>, cb: (...args: any[]) => void) {
    return {
        keys,
        render: cb
    }
}

const COLUMNS = [
    maker('a', (prop /** stirng */) => { }),
    maker(['a'], (prop /** Record<"a", string> */) => { }),
    maker('b', (prop /** Record<"a", string> */) => { })
] as const



操场


推荐阅读