Skip to content

Commit

Permalink
feat: Support for mapped types
Browse files Browse the repository at this point in the history
  • Loading branch information
Gerrit0 committed Oct 25, 2020
1 parent d52017a commit 1036069
Show file tree
Hide file tree
Showing 12 changed files with 496 additions and 65 deletions.
2 changes: 1 addition & 1 deletion src/index.ts
Expand Up @@ -5,7 +5,7 @@ export { EventDispatcher, Event } from "./lib/utils/events";
export { resetReflectionID } from "./lib/models/reflections/abstract";
export { normalizePath } from "./lib/utils/fs";
export * from "./lib/models/reflections";
export * from "./lib/output/plugins";
export { Converter } from "./lib/converter";
export { Renderer } from "./lib/output/renderer";
export {
DefaultTheme,
Expand Down
83 changes: 83 additions & 0 deletions src/lib/converter/types.ts
Expand Up @@ -20,6 +20,7 @@ import {
TypeOperatorType,
UnionType,
UnknownType,
MappedType,
} from "../models";
import { Context } from "./context";
import { Converter } from "./converter";
Expand Down Expand Up @@ -54,6 +55,7 @@ export function loadConverters() {
typeLiteralConverter,
referenceConverter,
namedTupleMemberConverter,
mappedConverter,
literalTypeConverter,
thisConverter,
tupleConverter,
Expand Down Expand Up @@ -444,6 +446,53 @@ const namedTupleMemberConverter: TypeConverter<ts.NamedTupleMember> = {
convertType: requestBugReport,
};

// { -readonly [K in string]-?: number}
// ^ readonlyToken
// ^ typeParameter
// ^^^^^^ typeParameter.constraint
// ^ questionToken
// ^^^^^^ type
const mappedConverter: TypeConverter<
ts.MappedTypeNode,
ts.Type & {
// Beware! Internal TS API here.
templateType: ts.Type;
typeParameter: ts.TypeParameter;
constraintType: ts.Type;
}
> = {
kind: [ts.SyntaxKind.MappedType],
convert(context, node) {
const optionalModifier = kindToModifier(node.questionToken?.kind);
const templateType = convertType(context, node.type);

return new MappedType(
node.typeParameter.name.text,
convertType(context, node.typeParameter.constraint),
optionalModifier === "+"
? removeUndefined(templateType)
: templateType,
kindToModifier(node.readonlyToken?.kind),
optionalModifier
);
},
convertType(context, type, node) {
// This can happen if a generic function does not have a return type annotated.
const optionalModifier = kindToModifier(node.questionToken?.kind);
const templateType = convertType(context, type.templateType);

return new MappedType(
type.typeParameter.symbol?.name,
convertType(context, type.typeParameter.getConstraint()),
optionalModifier === "+"
? removeUndefined(templateType)
: templateType,
kindToModifier(node.readonlyToken?.kind),
optionalModifier
);
},
};

const literalTypeConverter: TypeConverter<
ts.LiteralTypeNode,
ts.LiteralType
Expand Down Expand Up @@ -591,3 +640,37 @@ function requestBugReport(context: Context, nodeOrType: ts.Node | ts.Type) {
function isObjectType(type: ts.Type): type is ts.ObjectType {
return typeof (type as any).objectFlags === "number";
}

function kindToModifier(
kind:
| ts.SyntaxKind.PlusToken
| ts.SyntaxKind.MinusToken
| ts.SyntaxKind.ReadonlyKeyword
| ts.SyntaxKind.QuestionToken
| undefined
): "+" | "-" | undefined {
switch (kind) {
case ts.SyntaxKind.ReadonlyKeyword:
case ts.SyntaxKind.QuestionToken:
case ts.SyntaxKind.PlusToken:
return "+";
case ts.SyntaxKind.MinusToken:
return "-";
default:
return undefined;
}
}

function removeUndefined(type: Type) {
if (type instanceof UnionType) {
const types = type.types.filter(
(t) => !t.equals(new IntrinsicType("undefined"))
);
if (types.length === 1) {
return types[0];
}
type.types = types;
return type;
}
return type;
}
7 changes: 4 additions & 3 deletions src/lib/models/types/index.ts
Expand Up @@ -5,12 +5,13 @@ export { IndexedAccessType } from "./indexed-access";
export { InferredType } from "./inferred";
export { IntersectionType } from "./intersection";
export { IntrinsicType } from "./intrinsic";
export { QueryType } from "./query";
export { LiteralType } from "./literal";
export { MappedType } from "./mapped";
export { PredicateType } from "./predicate";
export { QueryType } from "./query";
export { ReferenceType } from "./reference";
export { ReflectionType } from "./reflection";
export { LiteralType } from "./literal";
export { TupleType, NamedTupleMember } from "./tuple";
export { NamedTupleMember, TupleType } from "./tuple";
export { TypeOperatorType } from "./type-operator";
export { TypeParameterType } from "./type-parameter";
export { UnionType } from "./union";
Expand Down
59 changes: 59 additions & 0 deletions src/lib/models/types/mapped.ts
@@ -0,0 +1,59 @@
import { Type } from "./abstract";

/**
* Represents a mapped type.
*
* ```ts
* { -readonly [K in keyof U]?: Foo }
* ```
*/
export class MappedType extends Type {
readonly type = "mapped";

constructor(
public parameter: string,
public parameterType: Type,
public templateType: Type,
public readonlyModifier?: "+" | "-",
public optionalModifier?: "+" | "-"
) {
super();
}

clone(): Type {
return new MappedType(
this.parameter,
this.parameterType.clone(),
this.templateType.clone(),
this.readonlyModifier,
this.optionalModifier
);
}

equals(other: Type): boolean {
return (
other instanceof MappedType &&
other.parameter == this.parameter &&
other.parameterType.equals(this.parameterType) &&
other.templateType.equals(this.templateType) &&
other.readonlyModifier === this.readonlyModifier &&
other.optionalModifier === this.optionalModifier
);
}

toString(): string {
const read = {
"+": "readonly",
"-": "-readonly",
"": "",
}[this.readonlyModifier ?? ""];

const opt = {
"+": "?",
"-": "-?",
"": "",
}[this.optionalModifier ?? ""];

return `{ ${read}[${this.parameter} in ${this.parameterType}]${opt}: ${this.templateType}}`;
}
}
12 changes: 12 additions & 0 deletions src/lib/serialization/schema.ts
Expand Up @@ -271,6 +271,18 @@ export interface NamedTupleMemberType
element: ModelToObject<M.NamedTupleMember["element"]>;
}

export interface MappedType
extends Type,
S<
M.MappedType,
| "type"
| "parameter"
| "parameterType"
| "templateType"
| "readonlyModifier"
| "optionalModifier"
> {}

export interface TypeOperatorType
extends Type,
S<M.TypeOperatorType, "type" | "operator" | "target"> {}
Expand Down
1 change: 1 addition & 0 deletions src/lib/serialization/serializer.ts
Expand Up @@ -138,6 +138,7 @@ const serializerComponents: (new (owner: Serializer) => SerializerComponent<
S.LiteralTypeSerializer,
S.TupleTypeSerializer,
S.NamedTupleMemberTypeSerializer,
S.MappedTypeSerializer,
S.TypeOperatorTypeSerializer,
S.TypeParameterTypeSerializer,
S.UnionTypeSerializer,
Expand Down
3 changes: 2 additions & 1 deletion src/lib/serialization/serializers/types/index.ts
Expand Up @@ -5,11 +5,12 @@ export * from "./indexed-access";
export * from "./inferred";
export * from "./intersection";
export * from "./intrinsic";
export * from "./literal";
export * from "./mapped";
export * from "./predicate";
export * from "./query";
export * from "./reference";
export * from "./reflection";
export * from "./literal";
export * from "./tuple";
export * from "./type-operator";
export * from "./type-parameter";
Expand Down
23 changes: 23 additions & 0 deletions src/lib/serialization/serializers/types/mapped.ts
@@ -0,0 +1,23 @@
import { TypeSerializerComponent } from "../..";
import { MappedType } from "../../../models";
import { MappedType as JSONMappedType } from "../../schema";

export class MappedTypeSerializer extends TypeSerializerComponent<MappedType> {
supports(t: unknown) {
return t instanceof MappedType;
}

toObject(
map: MappedType,
obj: Pick<JSONMappedType, "type">
): JSONMappedType {
return {
...obj,
parameter: map.parameter,
parameterType: this.owner.toObject(map.parameterType),
templateType: this.owner.toObject(map.templateType),
readonlyModifier: map.readonlyModifier,
optionalModifier: map.optionalModifier,
};
}
}
120 changes: 118 additions & 2 deletions src/test/converter/alias/specs.json
Expand Up @@ -319,8 +319,124 @@
"name": "PopFront"
},
{
"type": "unknown",
"name": "{\n [K in keyof R | keyof T[0]]: K extends keyof R ? R[K] : T[0][K];\n }"
"type": "mapped",
"parameter": "K",
"parameterType": {
"type": "union",
"types": [
{
"type": "typeOperator",
"operator": "keyof",
"target": {
"type": "typeParameter",
"name": "R",
"default": {
"type": "reflection",
"declaration": {
"id": 24,
"name": "__type",
"kind": 65536,
"kindString": "Type literal",
"flags": {}
}
}
}
},
{
"type": "typeOperator",
"operator": "keyof",
"target": {
"type": "indexedAccess",
"indexType": {
"type": "literal",
"value": 0
},
"objectType": {
"type": "typeParameter",
"name": "T",
"constraint": {
"type": "array",
"elementType": {
"type": "intrinsic",
"name": "any"
}
}
}
}
}
]
},
"templateType": {
"type": "conditional",
"checkType": {
"type": "reference",
"name": "K"
},
"extendsType": {
"type": "typeOperator",
"operator": "keyof",
"target": {
"type": "typeParameter",
"name": "R",
"default": {
"type": "reflection",
"declaration": {
"id": 24,
"name": "__type",
"kind": 65536,
"kindString": "Type literal",
"flags": {}
}
}
}
},
"trueType": {
"type": "indexedAccess",
"indexType": {
"type": "reference",
"name": "K"
},
"objectType": {
"type": "typeParameter",
"name": "R",
"default": {
"type": "reflection",
"declaration": {
"id": 24,
"name": "__type",
"kind": 65536,
"kindString": "Type literal",
"flags": {}
}
}
}
},
"falseType": {
"type": "indexedAccess",
"indexType": {
"type": "reference",
"name": "K"
},
"objectType": {
"type": "indexedAccess",
"indexType": {
"type": "literal",
"value": 0
},
"objectType": {
"type": "typeParameter",
"name": "T",
"constraint": {
"type": "array",
"elementType": {
"type": "intrinsic",
"name": "any"
}
}
}
}
}
}
}
],
"name": "HorribleRecursiveTypeThatShouldNotBeUsedByAnyone"
Expand Down
5 changes: 5 additions & 0 deletions src/test/converter/types/mapped.ts
@@ -0,0 +1,5 @@
export function mapped<T>(arg: T) {
return {} as { -readonly [K in keyof T]?: string };
}

export type Mappy<T> = { [K in keyof T]: T[K] };

0 comments on commit 1036069

Please sign in to comment.