简体   繁体   English

是否可以在 TypeScript 中没有完全应用类型级功能?

[英]Is it possible to have not fully applied type-level functions in TypeScript?

Let's say we have the following input JSON data:假设我们有以下输入 JSON 数据:

{
    "foo": 1,
    "bar": [2],
    "baz": [3],
}

Is it possible to convert this JSON to the following type:是否可以将此 JSON 转换为以下类型:

{
    foo: '1'
    bar: MyList<['2']>
    baz: MySet<['3']>
}

Based on the following data:基于以下数据:

{
    foo: new MyType<string>(),
    bar: new MyType<MyList<string[]>>(),
    baz: new MyType<MySet<string[]>>(),
}

The conversion from number to string of the number literals is just to indicate I plan to do some transformation there (though the actual types in my use case are more complex).number到数字文字的string的转换只是为了表明我计划在那里进行一些转换(尽管我的用例中的实际类型更复杂)。

I can get it to work to some degree with hardcoding the container types, eg MyList :我可以通过硬编码容器类型来使其在某种程度上起作用,例如MyList

const input = {
    foo: 1,
    bar: [2],
    baz: [3],
} as const;
type Input = typeof input;

class MyType<T> {}
type MyList<T> = Array<T>;
type MySet<T> = Set<T>;

const data = {
    foo: new MyType<string>(),
    bar: new MyType<MyList<string[]>>(),
    baz: new MyType<MySet<string[]>>(),
};
type Data = typeof data;

type ToString<N extends number> = `${N}`;

const result = {} as {
    [K in keyof Input]: [Input[K], Data[K]] extends [infer I, MyType<infer T>]
        ? T extends string
            ? I extends number
                ? ToString<I>
                : never
            : T extends MyList<string[]>
            ? I extends readonly number[]
                ? MyList<number[]>
                : never
            : T extends MySet<string[]>
            ? I extends readonly number[]
                ? MySet<number[]>
                : never
            : never
        : never;
};
// const result: {
//     readonly foo: "1";
//     readonly bar: MyList<number[]>;
//     readonly baz: MySet<number[]>;
// }

Would it be possible to retain the literal tuple and without hardcoding the container types?是否可以保留文字元组而不对容器类型进行硬编码?

With my current code, not hardcoding would just be a matter of (assuming safety is ensured through other means):使用我当前的代码,不是硬编码只是一个问题(假设通过其他方式确保安全):

const result = {} as {
    [K in keyof Input]: [Input[K], Data[K]] extends [infer I, MyType<infer T>]
        ? T extends string
            ? I extends number
                ? ToString<I>
                : never
            : T
        : never;
};
// const result: {
//     readonly foo: "1";
//     readonly bar: MyList<number[]>;
//     readonly baz: MySet<number[]>;
// }

If I could pass to MyType a type-level function that got as its argument the literal, it would be easy to implement these container types.如果我可以将一个类型级别的 function 传递给MyType ,它的参数是文字,那么实现这些容器类型将很容易。 However I have no idea if this is at all possible in TypeScript or if there are some other ways around it to get the intended result.但是我不知道这在 TypeScript 中是否有可能,或者是否有其他方法可以得到预期的结果。

Thanks to the helpful comment of @kelly pointing me towards a blog post explaining a trick to emulate higher kinded types to some degree, I managed to get it to work as intended.感谢@kelly 的有用评论将我指向一篇博客文章,该文章解释了在某种程度上模拟高级类型的技巧,我设法让它按预期工作。

The trick described in the blog post is the following:博客文章中描述的技巧如下:

interface IdFunction {
    readonly input: unknown
    readonly output: this['input']
}
type X = (IdFunction & { readonly input: 5 })['output']
// type X = 5

Using it, the type of the input can be given at a later point, making it act like a type-level function that still expects arguments.使用它,可以在稍后给出输入的类型,使其像类型级别的 function 一样仍然需要 arguments。

The key insight is that this refers to the final type (in this case IdFunction & { readonly input: 5 } , that resolves to { readonly input: 5; readonly output: this['input'] } ), making it possible to have multiple subtypes, yet when accessing them through the output property of the parent type, it will still provide the information given in the subtype.关键的见解是this指的是最终类型(在这种情况下IdFunction & { readonly input: 5 } ,它解析为{ readonly input: 5; readonly output: this['input'] } ),使其成为可能多个子类型,但是当通过父类型的output属性访问它们时,它仍然会提供子类型中给出的信息。

Given the example provided in my question, here is a full example of this trick being applied:鉴于我的问题中提供的示例,以下是应用此技巧的完整示例:

const input = {
    foo: 1,
    bar: [2],
    baz: [3],
} as const;
type Input = typeof input;

type NumberToString<N extends number> = `${N}`;
type NumbersToStrings<T extends readonly [...any[]]> = T extends readonly [infer Head, ...infer Tail]
    ? Head extends number
        ? [NumberToString<Head>, ...NumbersToStrings<Tail>]
        : [...NumbersToStrings<Tail>]
    : [];

class MappedType {
    readonly A: unknown;
    readonly B: unknown;
}
class MappedList extends MappedType {
    readonly A: [...number[]];
    readonly B: MyList<NumbersToStrings<this['A']>>;
}
class MappedSet extends MappedType {
    readonly A: [...number[]];
    readonly B: MySet<NumbersToStrings<this['A']>>;
}

class MyType<T> {}
type MyList<T> = Array<T>;
type MySet<T> = Set<T>;

const data = {
    foo: new MyType<string>(),
    bar: new MyType<MappedList>(),
    baz: new MyType<MappedSet>(),
};
type Data = typeof data;

const result = {} as {
    [K in keyof Input]: [Input[K], Data[K]] extends [infer I, MyType<infer T>]
        ? T extends string
            ? I extends number
                ? NumberToString<I>
                : never
            : T extends MappedType
            ? (T & { readonly A: I })['B']
            : never
        : never;
};
// const result: {
//     readonly foo: "1";
//     readonly bar: MyList<["2"]>;
//     readonly baz: MySet<["3"]>;
// }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM