Skip to content

Commit

Permalink
feat: Support for TS 4.1 mapped types + string literal types
Browse files Browse the repository at this point in the history
Resolves #1397
  • Loading branch information
Gerrit0 committed Nov 26, 2020
1 parent bf67230 commit a32c976
Show file tree
Hide file tree
Showing 59 changed files with 3,491 additions and 172 deletions.
8 changes: 4 additions & 4 deletions examples/basic/src/flattened.ts
@@ -1,7 +1,7 @@
/**
* A class that contains members with flattened properties.
*/
class FlattenedClass {
export class FlattenedClass {
/**
* A member that accepts an option object defined inline.
*
Expand Down Expand Up @@ -92,7 +92,7 @@ class FlattenedClass {
* @param callback.param A parameter of the typed function callback.
* @param callback.optionalParam An optional parameter of the typed function callback.
*/
function flattenedCallback(
export function flattenedCallback(
callback: (param: number, optionalParam?: string) => string
) {}

Expand All @@ -105,7 +105,7 @@ function flattenedCallback(
* @param options.moreOptions A typed child object of the options object.
* @param options.moreOptions.moreValues A value of the typed child object.
*/
function flattenedParameter(options: {
export function flattenedParameter(options: {
[name: string]: any;
value?: string;
anotherValue?: string;
Expand All @@ -121,7 +121,7 @@ function flattenedParameter(options: {
* @param indexed.index The index property comment.
* @param indexed.test A property of the index signature instance.
*/
function flattenedIndexSignature(indexed: {
export function flattenedIndexSignature(indexed: {
[index: number]: { name: string; value?: number };
test: string;
}) {}
24 changes: 15 additions & 9 deletions examples/basic/src/generics.ts
Expand Up @@ -5,7 +5,7 @@
* @param value A generic parameter.
* @returns A generic return value.
*/
function testFunction<T>(value: T): T {
export function testFunction<T>(value: T): T {
return value;
}

Expand All @@ -14,7 +14,7 @@ function testFunction<T>(value: T): T {
*
* @param T The generic type parameter.
*/
interface A<T> {
export interface A<T> {
/**
* A generic member function.
*
Expand All @@ -29,7 +29,7 @@ interface A<T> {
* @param <T> The first generic type parameter.
* @param <C> The second generic type parameter.
*/
interface B<T, C> {
export interface B<T, C> {
/**
* A generic member function.
*
Expand All @@ -51,31 +51,31 @@ interface B<T, C> {
*
* @typeparam T The leftover generic type parameter.
*/
interface AB<T> extends A<T>, B<T, boolean> {}
export interface AB<T> extends A<T>, B<T, boolean> {}

/**
* An interface extending a generic interface and setting its type parameter.
*/
interface ABString extends AB<string> {}
export interface ABString extends AB<string> {}

/**
* An interface extending a generic interface and setting its type parameter.
*/
interface ABNumber extends AB<number> {}
export interface ABNumber extends AB<number> {}

/**
* A function returning a generic array with type parameters.
*
* @return The return value with type arguments.
*/
function getGenericArray(): Array<string> {
export function getGenericArray(): Array<string> {
return [""];
}

/**
* Conditional type with infer
*/
type PopFront<T extends any[]> = ((...args: T) => any) extends (
export type PopFront<T extends any[]> = ((...args: T) => any) extends (
a: any,
...r: infer R
) => any
Expand All @@ -86,7 +86,7 @@ type PopFront<T extends any[]> = ((...args: T) => any) extends (
* See GH#1150. Calling typeChecker.typeToString on this type will send TS into an infinite
* loop, which is undesirable.
*/
type HorribleRecursiveTypeThatShouldNotBeUsedByAnyone<
export type HorribleRecursiveTypeThatShouldNotBeUsedByAnyone<
T extends any[],
R = {}
> = {
Expand All @@ -98,3 +98,9 @@ type HorribleRecursiveTypeThatShouldNotBeUsedByAnyone<
}
>;
}[T["length"] extends 0 ? 0 : 1];

export type DoubleKey<T> = { [K in keyof T & string as `${K}${K}`]: T[K] }

export function doubleKey<T>(arg: T) {
return {} as { [K in keyof T & string as `${K}${K}`]: T[K] }
}
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -30,7 +30,7 @@
"progress": "^2.0.3",
"semver": "^7.3.2",
"shelljs": "^0.8.4",
"typedoc-default-themes": "0.12.0-beta.6"
"typedoc-default-themes": "0.12.0-beta.7"
},
"peerDependencies": {
"typescript": "3.9.x || 4.0.x || 4.1.x"
Expand Down
37 changes: 35 additions & 2 deletions src/lib/converter/types.ts
Expand Up @@ -22,6 +22,8 @@ import {
UnknownType,
MappedType,
} from "../models";
import { TemplateLiteralType } from "../models/types/template-literal";
import { zip } from "../utils/array";
import { Context } from "./context";
import { ConverterEvents } from "./converter-events";
import { createSignature } from "./factories/signature";
Expand Down Expand Up @@ -68,6 +70,7 @@ export function loadConverters() {
namedTupleMemberConverter,
mappedConverter,
literalTypeConverter,
templateLiteralConverter,
thisConverter,
tupleConverter,
typeOperatorConverter,
Expand Down Expand Up @@ -500,6 +503,7 @@ const mappedConverter: TypeConverter<
templateType: ts.Type;
typeParameter: ts.TypeParameter;
constraintType: ts.Type;
nameType?: ts.Type;
}
> = {
kind: [ts.SyntaxKind.MappedType],
Expand All @@ -514,7 +518,8 @@ const mappedConverter: TypeConverter<
? removeUndefined(templateType)
: templateType,
kindToModifier(node.readonlyToken?.kind),
optionalModifier
optionalModifier,
node.nameType ? convertType(context, node.nameType, target) : void 0
);
},
convertType(context, type, node, target) {
Expand All @@ -529,7 +534,8 @@ const mappedConverter: TypeConverter<
? removeUndefined(templateType)
: templateType,
kindToModifier(node.readonlyToken?.kind),
optionalModifier
optionalModifier,
type.nameType ? convertType(context, type.nameType, target) : void 0
);
},
};
Expand Down Expand Up @@ -601,6 +607,33 @@ const literalTypeConverter: TypeConverter<
},
};

const templateLiteralConverter: TypeConverter<
ts.TemplateLiteralTypeNode,
ts.TemplateLiteralType
> = {
kind: [ts.SyntaxKind.TemplateLiteralType],
convert(context, node, target) {
return new TemplateLiteralType(
node.head.text,
node.templateSpans.map((span) => {
return [
convertType(context, span.type, target),
span.literal.text,
];
})
);
},
convertType(context, type, _node, target) {
assert(type.texts.length === type.types.length + 1);
const parts: [Type, string][] = [];
for (const [a, b] of zip(type.types, type.texts.slice(1))) {
parts.push([convertType(context, a, target), b]);
}

return new TemplateLiteralType(type.texts[0], parts);
},
};

const thisConverter: TypeConverter<ts.ThisTypeNode> = {
kind: [ts.SyntaxKind.ThisType],
convert() {
Expand Down
1 change: 1 addition & 0 deletions src/lib/models/types/index.ts
Expand Up @@ -11,6 +11,7 @@ export { PredicateType } from "./predicate";
export { QueryType } from "./query";
export { ReferenceType } from "./reference";
export { ReflectionType } from "./reflection";
export { TemplateLiteralType } from "./template-literal";
export { NamedTupleMember, TupleType } from "./tuple";
export { TypeOperatorType } from "./type-operator";
export { TypeParameterType } from "./type-parameter";
Expand Down
24 changes: 20 additions & 4 deletions src/lib/models/types/mapped.ts
Expand Up @@ -4,7 +4,7 @@ import { Type } from "./abstract";
* Represents a mapped type.
*
* ```ts
* { -readonly [K in keyof U]?: Foo }
* { -readonly [K in keyof U & string as `a${K}`]?: Foo }
* ```
*/
export class MappedType extends Type {
Expand All @@ -15,7 +15,8 @@ export class MappedType extends Type {
public parameterType: Type,
public templateType: Type,
public readonlyModifier?: "+" | "-",
public optionalModifier?: "+" | "-"
public optionalModifier?: "+" | "-",
public nameType?: Type
) {
super();
}
Expand All @@ -26,11 +27,24 @@ export class MappedType extends Type {
this.parameterType.clone(),
this.templateType.clone(),
this.readonlyModifier,
this.optionalModifier
this.optionalModifier,
this.nameType?.clone()
);
}

equals(other: Type): boolean {
if (!(other instanceof MappedType)) {
return false;
}

if (this.nameType && other.nameType) {
if (!this.nameType.equals(other.nameType)) {
return false;
}
} else if (this.nameType !== other.nameType) {
return false;
}

return (
other instanceof MappedType &&
other.parameter == this.parameter &&
Expand All @@ -54,6 +68,8 @@ export class MappedType extends Type {
"": "",
}[this.optionalModifier ?? ""];

return `{ ${read}[${this.parameter} in ${this.parameterType}]${opt}: ${this.templateType}}`;
const name = this.nameType ? ` as ${this.nameType}` : "";

return `{ ${read}[${this.parameter} in ${this.parameterType}${name}]${opt}: ${this.templateType}}`;
}
}
51 changes: 51 additions & 0 deletions src/lib/models/types/template-literal.ts
@@ -0,0 +1,51 @@
import { Type } from "./abstract";

/**
* TS 4.1 template literal types
* ```ts
* type Z = `${'a' | 'b'}${'a' | 'b'}`
* ```
*/
export class TemplateLiteralType extends Type {
readonly type = "template-literal";

head: string;
tail: [Type, string][];

constructor(head: string, tail: [Type, string][]) {
super();
this.head = head;
this.tail = tail;
}

clone(): Type {
return new TemplateLiteralType(
this.head,
this.tail.map(([type, text]) => [type.clone(), text])
);
}

equals(other: Type): boolean {
return (
other instanceof TemplateLiteralType &&
this.head === other.head &&
this.tail.length === other.tail.length &&
this.tail.every(([type, text], i) => {
return (
type.equals(other.tail[i][0]) && text === other.tail[i][1]
);
})
);
}

toString() {
return [
"`",
this.head,
...this.tail.map(([type, text]) => {
return "${" + type + "}" + text;
}),
"`",
].join("");
}
}
8 changes: 8 additions & 0 deletions src/lib/serialization/schema.ts
Expand Up @@ -88,6 +88,8 @@ type _ModelToObject<T> =
? TupleType
: T extends M.UnknownType
? UnknownType
: T extends M.TemplateLiteralType
? TemplateLiteralType
: T extends M.Type
? SomeType // Technically AbstractType, but the union is more useful
: // Miscellaneous
Expand Down Expand Up @@ -285,6 +287,11 @@ export interface NamedTupleMemberType
element: ModelToObject<M.NamedTupleMember["element"]>;
}

export interface TemplateLiteralType
extends Type,
S<M.TemplateLiteralType, "type" | "head"> {
tail: [Type, string][];
}
export interface MappedType
extends Type,
S<
Expand All @@ -295,6 +302,7 @@ export interface MappedType
| "templateType"
| "readonlyModifier"
| "optionalModifier"
| "nameType"
> {}

export interface TypeOperatorType
Expand Down
1 change: 1 addition & 0 deletions src/lib/serialization/serializer.ts
Expand Up @@ -148,6 +148,7 @@ const serializerComponents: (new (owner: Serializer) => SerializerComponent<
S.ReflectionTypeSerializer,
S.LiteralTypeSerializer,
S.TupleTypeSerializer,
S.TemplateLiteralTypeSerializer,
S.NamedTupleMemberTypeSerializer,
S.MappedTypeSerializer,
S.TypeOperatorTypeSerializer,
Expand Down
1 change: 1 addition & 0 deletions src/lib/serialization/serializers/types/index.ts
Expand Up @@ -11,6 +11,7 @@ export * from "./predicate";
export * from "./query";
export * from "./reference";
export * from "./reflection";
export * from "./template-literal";
export * from "./tuple";
export * from "./type-operator";
export * from "./type-parameter";
Expand Down
1 change: 1 addition & 0 deletions src/lib/serialization/serializers/types/mapped.ts
Expand Up @@ -18,6 +18,7 @@ export class MappedTypeSerializer extends TypeSerializerComponent<MappedType> {
templateType: this.owner.toObject(map.templateType),
readonlyModifier: map.readonlyModifier,
optionalModifier: map.optionalModifier,
nameType: this.owner.toObject(map.nameType),
};
}
}

0 comments on commit a32c976

Please sign in to comment.