TypeScript 中循环类型之间的映射
Mapping Between Circular Types in TypeScript
这个问题是关于静态推断运行时类型的签名(见于 zod and io-ts 等库)。
看到下面的例子。
假设我们正在尝试为运行时使用的某些类型信息建模。我们可以声明以下 Type
枚举来让我们开始:
enum Type {
Boolean = "Boolean",
Int = "Int",
List = "List",
Union = "Union",
}
我们的运行时类型系统应该支持布尔值、整数、联合和列表。
基本类型如下所示:
interface Codec<T extends Type> {
type: T;
}
布尔和整数类型像这样使用该基类型。
布尔值:
class BooleanCodec implements Codec<Type.Boolean> {
type = Type.Boolean as const;
}
整数:
class IntCodec implements Codec<Type.Int> {
type = Type.Int as const;
}
联合类型接受一个类型数组来联合:
class UnionCodec<C extends Codec<Type>> implements Codec<Type.Union> {
type = Type.Union as const;
constructor(public of: C[]) {}
}
列表类型接受其元素组成的类型:
class ListCodec<C extends Codec<Type>> implements Codec<Type.List> {
type = Type.List as const;
constructor(public of: C) {}
}
让我们构造一个布尔值或整数列表:
const listOfBooleanOrIntCodec = new ListCodec(
new UnionCodec([
new BooleanCodec(),
new IntCodec(),
]),
);
计算结果为以下对象:
{
type: Type.List,
of: {
type: Type.Union,
of: [
{
type: Type.Boolean,
},
{
type: Type.Int,
},
]
}
}
此编解码器将带有 ListCodec<UnionCodec<BooleanCodec | IntCodec>>
的签名。
我们甚至可能在给定的编解码器中看到循环,因此,映射类型签名变得很棘手。我们如何从上面得到 (boolean | number)[]
?它是否考虑了编解码器的深度嵌套?
对于 BooleanCodec
或 IntCodec
,向后工作相当容易...但是 UnionCodec
和 ListCodec
解码需要递归。我尝试了以下方法:
type Decode<C extends Codec<Type>> =
// if it's a list
C extends ListCodec<Codec<Type>>
? // and we can infer what it's a list of
C extends ListCodec<infer O>
? // and the elements are of type codec
O extends Codec<Type>
? // recurse to get an array of the element(s') type
Decode<O>[]
: never
: never
: // if it's a union
C extends UnionCodec<Codec<Type>>
// and we can infer what it's a union of
? C extends UnionCodec<infer U>
// and it's a union of codecs
? U extends Codec<Type>
// recurse to return that type (which will be inferred as the union)
? Decode<U>
: never
: never
// if it's a boolean codec
: C extends BooleanCodec
// return the boolean type
? boolean
// if it's ant integer codec
: C extends IntCodec
// return the number type
? number
: never;
遗憾的是 Type alias 'Decode' circularly references itself
和 Type 'Decode' is not generic
错误。
我想知道是否有可能完成这种循环类型映射,以及如何使 Decode
这样的实用程序工作?任何帮助将不胜感激。谢谢!
我通常定义类型,然后从中派生通用编解码器,而不是显式构建编解码器。
例如:首先,用一些数据定义类型并对它们的关系(列表项和联合值)进行编码:
type Type = Integer | List<any> | Union<any>;
interface Integer {
type: 'integer';
}
interface List<T extends Type> {
type: 'list';
item: T;
}
type UnionValues = Type[];
interface Union<T extends UnionValues> {
type: 'union';
values: T;
}
很高兴还提供了创建这些类型的助手:
const integer: Integer = { type: 'integer' };
const list = <T extends Type>(item: T): List<T> => ({
type: 'list',
item
});
const union = <T extends UnionValues>(...values: T): Union<T> => ({
type: 'union',
values
});
然后您可以编写递归类型映射函数。这会将 Type
映射到其对应的 JS 类型:
type Decode<T> =
// terminal recursion: Integer is represented as a number
T extends Integer ? number :
// extract the Item from the list and construct an Array recursively
T extends List<infer I> ? Decode<I>[] :
// union is an array of types, so loop through and decode them
T extends Union<infer U> ? {
[i in Extract<keyof U, number>]: Decode<U[i]>;
}[[Extract<keyof U, number>]] :
never
;
将您的编解码器定义为从 Type => Value
:
读取
interface Codec<T extends Type, V> {
type: T;
read(value: any): V;
}
编写一个将类型实例映射到其编解码器的函数:
function codec<T extends Type>(type: T): Codec<T, Decode<T>> {
// todo
}
现在您可以安全地在类型系统和 JS 类型之间进行映射:
const i = codec(integer);
const number: number = i.read('1');
const l = codec(list(integer));
const numberArray: number[] = l.read('[1, 2]');
const u = codec(union(integer, list(integer)));
const numberOrArrayOfNumbers: number | number[] = u.read('1');
我试图重新创建您的示例,其中开发人员编写对其类型进行编码的编解码器。我认为这大致就是您要尝试做的事情。这有点复杂,因为您需要映射元组。
整数编解码器是整数 -> 数字的直接映射。
class IntegerCodec implements Codec<Integer, number> {
public readonly type: Integer = integer;
public read(value: any): number {
return parseInt(value, 10);
}
}
ListCodec 递归计算 List -> ItemValue[] 的映射
namespace Codec {
// helper type function for grabbing the JS type from a Codec<any, any>
export type GetValue<C extends Codec<any, any>> = C extends Codec<any, infer V> ? V : never;
}
// this is where we recurse and compute the Type and JSType from the provided Item codec
class ListCodec<Item extends Codec<any, any>> implements Codec<List<Item['type']>, Codec.GetValue<Item>[]> {
public readonly type: List<Item['type']>;
constructor(public readonly item: Item) {
this.type = list(item.type);
}
public read(value: any): Codec.GetValue<Item>[] {
return value.map((v: any) => this.item.read(v));
}
}
联合有点困难,因为我们需要映射一个编解码器元组来计算类型和值。
第一个实用程序:从编解码器的元组计算联合类型
type ComputeUnionType<V extends Codec<any, any>[]> = Union<Type[] & {
[i in Extract<keyof V, number>]: V[i]['type']
}>;
第二个实用程序:从编解码器元组计算联合 JS 类型:
type ComputeUnionValue<V extends Codec<any, any>[]> = {
[i in Extract<keyof V, number>]: Codec.GetValue<V[i]>;
}[Extract<keyof V, number>];
然后我们写一个 UnionCodec 递归计算一个 Union 的 Type 和 JS Type:
class UnionCodec<V extends Codec<any, any>[]> implements Codec<
ComputeUnionType<V>,
ComputeUnionValue<V>
> {
public readonly type: ComputeUnionType<V>;
constructor(public readonly codecs: V) {}
public read(value: any): ComputeUnionValue<V> {
throw new Error("Method not implemented.");
}
}
现在您的示例类型检查:
const ic = new IntegerCodec();
const lc: ListCodec<IntegerCodec> = new ListCodec(new IntegerCodec());
const uc: UnionCodec<[ListCodec<IntegerCodec>, IntegerCodec]> = new UnionCodec([lc, ic]);
const listValue: number | number[] = uc.read('1');
这个问题是关于静态推断运行时类型的签名(见于 zod and io-ts 等库)。
看到下面的例子。假设我们正在尝试为运行时使用的某些类型信息建模。我们可以声明以下 Type
枚举来让我们开始:
enum Type {
Boolean = "Boolean",
Int = "Int",
List = "List",
Union = "Union",
}
我们的运行时类型系统应该支持布尔值、整数、联合和列表。
基本类型如下所示:
interface Codec<T extends Type> {
type: T;
}
布尔和整数类型像这样使用该基类型。
布尔值:
class BooleanCodec implements Codec<Type.Boolean> {
type = Type.Boolean as const;
}
整数:
class IntCodec implements Codec<Type.Int> {
type = Type.Int as const;
}
联合类型接受一个类型数组来联合:
class UnionCodec<C extends Codec<Type>> implements Codec<Type.Union> {
type = Type.Union as const;
constructor(public of: C[]) {}
}
列表类型接受其元素组成的类型:
class ListCodec<C extends Codec<Type>> implements Codec<Type.List> {
type = Type.List as const;
constructor(public of: C) {}
}
让我们构造一个布尔值或整数列表:
const listOfBooleanOrIntCodec = new ListCodec(
new UnionCodec([
new BooleanCodec(),
new IntCodec(),
]),
);
计算结果为以下对象:
{
type: Type.List,
of: {
type: Type.Union,
of: [
{
type: Type.Boolean,
},
{
type: Type.Int,
},
]
}
}
此编解码器将带有 ListCodec<UnionCodec<BooleanCodec | IntCodec>>
的签名。
我们甚至可能在给定的编解码器中看到循环,因此,映射类型签名变得很棘手。我们如何从上面得到 (boolean | number)[]
?它是否考虑了编解码器的深度嵌套?
对于 BooleanCodec
或 IntCodec
,向后工作相当容易...但是 UnionCodec
和 ListCodec
解码需要递归。我尝试了以下方法:
type Decode<C extends Codec<Type>> =
// if it's a list
C extends ListCodec<Codec<Type>>
? // and we can infer what it's a list of
C extends ListCodec<infer O>
? // and the elements are of type codec
O extends Codec<Type>
? // recurse to get an array of the element(s') type
Decode<O>[]
: never
: never
: // if it's a union
C extends UnionCodec<Codec<Type>>
// and we can infer what it's a union of
? C extends UnionCodec<infer U>
// and it's a union of codecs
? U extends Codec<Type>
// recurse to return that type (which will be inferred as the union)
? Decode<U>
: never
: never
// if it's a boolean codec
: C extends BooleanCodec
// return the boolean type
? boolean
// if it's ant integer codec
: C extends IntCodec
// return the number type
? number
: never;
遗憾的是 Type alias 'Decode' circularly references itself
和 Type 'Decode' is not generic
错误。
我想知道是否有可能完成这种循环类型映射,以及如何使 Decode
这样的实用程序工作?任何帮助将不胜感激。谢谢!
我通常定义类型,然后从中派生通用编解码器,而不是显式构建编解码器。
例如:首先,用一些数据定义类型并对它们的关系(列表项和联合值)进行编码:
type Type = Integer | List<any> | Union<any>;
interface Integer {
type: 'integer';
}
interface List<T extends Type> {
type: 'list';
item: T;
}
type UnionValues = Type[];
interface Union<T extends UnionValues> {
type: 'union';
values: T;
}
很高兴还提供了创建这些类型的助手:
const integer: Integer = { type: 'integer' };
const list = <T extends Type>(item: T): List<T> => ({
type: 'list',
item
});
const union = <T extends UnionValues>(...values: T): Union<T> => ({
type: 'union',
values
});
然后您可以编写递归类型映射函数。这会将 Type
映射到其对应的 JS 类型:
type Decode<T> =
// terminal recursion: Integer is represented as a number
T extends Integer ? number :
// extract the Item from the list and construct an Array recursively
T extends List<infer I> ? Decode<I>[] :
// union is an array of types, so loop through and decode them
T extends Union<infer U> ? {
[i in Extract<keyof U, number>]: Decode<U[i]>;
}[[Extract<keyof U, number>]] :
never
;
将您的编解码器定义为从 Type => Value
:
interface Codec<T extends Type, V> {
type: T;
read(value: any): V;
}
编写一个将类型实例映射到其编解码器的函数:
function codec<T extends Type>(type: T): Codec<T, Decode<T>> {
// todo
}
现在您可以安全地在类型系统和 JS 类型之间进行映射:
const i = codec(integer);
const number: number = i.read('1');
const l = codec(list(integer));
const numberArray: number[] = l.read('[1, 2]');
const u = codec(union(integer, list(integer)));
const numberOrArrayOfNumbers: number | number[] = u.read('1');
我试图重新创建您的示例,其中开发人员编写对其类型进行编码的编解码器。我认为这大致就是您要尝试做的事情。这有点复杂,因为您需要映射元组。
整数编解码器是整数 -> 数字的直接映射。
class IntegerCodec implements Codec<Integer, number> {
public readonly type: Integer = integer;
public read(value: any): number {
return parseInt(value, 10);
}
}
ListCodec 递归计算 List -> ItemValue[] 的映射
namespace Codec {
// helper type function for grabbing the JS type from a Codec<any, any>
export type GetValue<C extends Codec<any, any>> = C extends Codec<any, infer V> ? V : never;
}
// this is where we recurse and compute the Type and JSType from the provided Item codec
class ListCodec<Item extends Codec<any, any>> implements Codec<List<Item['type']>, Codec.GetValue<Item>[]> {
public readonly type: List<Item['type']>;
constructor(public readonly item: Item) {
this.type = list(item.type);
}
public read(value: any): Codec.GetValue<Item>[] {
return value.map((v: any) => this.item.read(v));
}
}
联合有点困难,因为我们需要映射一个编解码器元组来计算类型和值。
第一个实用程序:从编解码器的元组计算联合类型
type ComputeUnionType<V extends Codec<any, any>[]> = Union<Type[] & {
[i in Extract<keyof V, number>]: V[i]['type']
}>;
第二个实用程序:从编解码器元组计算联合 JS 类型:
type ComputeUnionValue<V extends Codec<any, any>[]> = {
[i in Extract<keyof V, number>]: Codec.GetValue<V[i]>;
}[Extract<keyof V, number>];
然后我们写一个 UnionCodec 递归计算一个 Union 的 Type 和 JS Type:
class UnionCodec<V extends Codec<any, any>[]> implements Codec<
ComputeUnionType<V>,
ComputeUnionValue<V>
> {
public readonly type: ComputeUnionType<V>;
constructor(public readonly codecs: V) {}
public read(value: any): ComputeUnionValue<V> {
throw new Error("Method not implemented.");
}
}
现在您的示例类型检查:
const ic = new IntegerCodec();
const lc: ListCodec<IntegerCodec> = new ListCodec(new IntegerCodec());
const uc: UnionCodec<[ListCodec<IntegerCodec>, IntegerCodec]> = new UnionCodec([lc, ic]);
const listValue: number | number[] = uc.read('1');