diff --git a/src/Escalier.Data/Library.fs b/src/Escalier.Data/Library.fs index f6f9c7f..da5b361 100644 --- a/src/Escalier.Data/Library.fs +++ b/src/Escalier.Data/Library.fs @@ -1096,7 +1096,14 @@ module Type = | TypeKind.Unary(op, arg) -> $"{op}{printType ctx arg}" | TypeKind.Wildcard -> "_" | TypeKind.TemplateLiteral { Parts = parts; Exprs = types } -> - failwith "TODO: printType - TypeKind.TemplateLiteral" + let mutable output = "" + + for part, t in List.zip (List.take types.Length parts) types do + output <- output + part + $"${{{printType ctx t}}}" + + output <- output + List.last parts + + $"`{output}`" | TypeKind.Intrinsic -> "intrinsic" | TypeKind.IntrinsicInstance { Name = name; TypeArgs = typeArgs } -> let typeArgs = diff --git a/src/Escalier.TypeChecker.Tests/Tests.fs b/src/Escalier.TypeChecker.Tests/Tests.fs index 9cc911c..c624db2 100644 --- a/src/Escalier.TypeChecker.Tests/Tests.fs +++ b/src/Escalier.TypeChecker.Tests/Tests.fs @@ -863,20 +863,29 @@ let InferTemplateLiteralTypeError () = Assert.True(Result.isError result) [] -let InferTemplateLiteralTypeErrorWithUnion () = +let InferTemplateLiteralWithUnion () = let result = result { let src = """ type Dir = `${"top" | "bottom"}-${"left" | "right"}`; - let x: Dir = "top-bottom"; + let x: Dir = "top-right"; """ - let! _, _ = inferModule src - () + let! ctx, env = inferModule src + + let! t = + expandScheme ctx env None (env.FindScheme "Dir") Map.empty None + |> Result.mapError CompileError.TypeError + + Assert.Equal( + "`${(\"top\" | \"bottom\")}-${(\"left\" | \"right\")}`", + t.ToString() + ) } - Assert.True(Result.isError result) + printfn "result = %A" result + Assert.False(Result.isError result) [] let InferIntrinsicsBasicUsageLiterals () = @@ -989,6 +998,93 @@ let InferIntrinsicsInTemplateLiteralTypesWithUnion () = printfn "result = %A" result Assert.False(Result.isError result) +[] +let KebabTemplateLiteralType () = + let result = + result { + + let src = + """ + type Kebab = `-${T}-${U}-`; + type Foo = Kebab<"hello", "world">; + type Bar = Kebab; + """ + + let! ctx, env = inferModule src + + Assert.Empty(ctx.Report.Diagnostics) + + let! t = + expandScheme ctx env None (env.FindScheme "Foo") Map.empty None + |> Result.mapError CompileError.TypeError + + Assert.Equal("\"-hello-world-\"", t.ToString()) + + let! t = + expandScheme ctx env None (env.FindScheme "Bar") Map.empty None + |> Result.mapError CompileError.TypeError + + Assert.Equal("`-${T}-${U}-`", t.ToString()) + } + + printfn "result = %A" result + Assert.False(Result.isError result) + +[] +let MappedTypeWithTemplateLiteralKey () = + let result = + result { + + let src = + """ + type Foo = { + [`_${K}`]: T[K] for K in keyof T, + }; + type Point = {x: number, y: number}; + type Bar = Foo; + """ + + let! ctx, env = inferModule src + + Assert.Empty(ctx.Report.Diagnostics) + + let! t = + expandScheme ctx env None (env.FindScheme "Bar") Map.empty None + |> Result.mapError CompileError.TypeError + + Assert.Equal("{_x: number, _y: number}", t.ToString()) + } + + printfn "result = %A" result + Assert.False(Result.isError result) + +[] +let MappedTypeWithTemplateLiteralKeyWithIntrinsic () = + let result = + result { + + let src = + """ + type Foo = { + [`_${Uppercase}`]: T[K] for K in keyof T, + }; + type Point = {x: number, y: number, [Symbol.iterator]: number}; + type Bar = Foo; + """ + + let! ctx, env = inferModule src + + Assert.Empty(ctx.Report.Diagnostics) + + let! t = + expandScheme ctx env None (env.FindScheme "Bar") Map.empty None + |> Result.mapError CompileError.TypeError + + Assert.Equal("{_X: number, _Y: number}", t.ToString()) + } + + Assert.False(Result.isError result) + [] let InferUnaryOperations () = let result = @@ -1935,7 +2031,7 @@ let InferMappedObjectType () = type Foo = { [K]: T[K][] for K in keyof T }; - type Bar = {a: string, b: number}; + type Bar = {a: string, b: number, [Symbol.iterator]: boolean}; type Baz = Foo; """ @@ -1948,7 +2044,11 @@ let InferMappedObjectType () = expandScheme ctx env None (env.FindScheme "Baz") Map.empty None |> Result.mapError CompileError.TypeError - Assert.Equal("{a: string[], b: number[]}", t.ToString()) + // TODO: maintain the original name of the symbol + Assert.Equal( + "{a: string[], b: number[], [Symbol(147)]: boolean[]}", + t.ToString() + ) } printfn "result = %A" result diff --git a/src/Escalier.TypeChecker/Unify.fs b/src/Escalier.TypeChecker/Unify.fs index 26c813e..29bea3e 100644 --- a/src/Escalier.TypeChecker/Unify.fs +++ b/src/Escalier.TypeChecker/Unify.fs @@ -1224,7 +1224,11 @@ module rec Unify = // TODO: Handle the case where the type is a primitive and use a // special function to expand the type // TODO: Handle different kinds of index types, e.g. number, symbol - return! Error(TypeError.NotImplemented "TODO: expand index") + return! + Error( + TypeError.NotImplemented + $"TODO: expand index - target type = {target}" + ) | TypeKind.Condition { Check = check Extends = extends TrueType = trueType @@ -1346,51 +1350,64 @@ module rec Unify = // do this. match c.Kind with | TypeKind.Union types -> - let! elems = - types - |> List.traverseResultM (fun keyType -> - result { - let propName = - match keyType.Kind with - | TypeKind.Literal(Literal.String name) -> - PropName.String name - | TypeKind.Literal(Literal.Number name) -> - PropName.Number name - | TypeKind.UniqueSymbol id -> PropName.Symbol id - | _ -> failwith $"Invalid key type {keyType}" - - let typeAnn = m.TypeAnn - - let folder t = - match t.Kind with - | TypeKind.TypeRef({ Name = QualifiedIdent.Ident name }) when - name = m.TypeParam.Name - -> - Some(keyType) - | _ -> None - - let typeAnn = foldType folder typeAnn - let! t = expandType ctx env ips mapping typeAnn - - let optional = - match m.Optional with - | None -> false // TODO: copy value from typeAnn if it's an index access type - | Some MappedModifier.Add -> true - | Some MappedModifier.Remove -> false - - let readonly = - match m.Readonly with - | None -> false // TODO: copy value from typeAnn if it's an index access type - | Some MappedModifier.Add -> true - | Some MappedModifier.Remove -> false - - return - Property - { Name = propName - Type = t - Optional = optional - Readonly = readonly } - }) + let mutable elems = [] + + for keyType in types do + let typeAnn = m.TypeAnn + + let folder t = + match t.Kind with + | TypeKind.TypeRef({ Name = QualifiedIdent.Ident name }) when + name = m.TypeParam.Name + -> + Some(keyType) + | _ -> None + + let typeAnn = foldType folder typeAnn + let! t = expandType ctx env ips mapping typeAnn + + let optional = + match m.Optional with + | None -> false // TODO: copy value from typeAnn if it's an index access type + | Some MappedModifier.Add -> true + | Some MappedModifier.Remove -> false + + let readonly = + match m.Readonly with + | None -> false // TODO: copy value from typeAnn if it's an index access type + | Some MappedModifier.Add -> true + | Some MappedModifier.Remove -> false + + let mutable nameMapping: Map = Map.empty + + nameMapping <- + Map.add m.TypeParam.Name keyType nameMapping + + let! keyType = + match m.NameType with + | Some nameType -> + expandType ctx env ips nameMapping nameType + | None -> Result.Ok keyType + + let propName = + match keyType.Kind with + | TypeKind.Literal(Literal.String name) -> + Some(PropName.String name) + | TypeKind.Literal(Literal.Number name) -> + Some(PropName.Number name) + | TypeKind.UniqueSymbol id -> Some(PropName.Symbol id) + | _ -> None + + match propName with + | Some propName -> + elems <- + elems + @ [ Property + { Name = propName + Type = t + Optional = optional + Readonly = readonly } ] + | _ -> () // Omits entries with other key types return elems | TypeKind.Literal(Literal.String key) -> @@ -1644,6 +1661,76 @@ module rec Unify = return { Kind = TypeKind.Tuple { Elems = elems; Immutable = immutable } Provenance = None } + | TypeKind.TemplateLiteral { Exprs = elems; Parts = quasis } -> + let! elems = elems |> List.traverseResultM (expand mapping) + + // TODO: check for other types that can't appear within template literal types + let isSymbol t = + match t.Kind with + | TypeKind.UniqueSymbol _ -> true + | _ -> false + + if List.exists isSymbol elems then + // We don't bother reporting an error because template literal types + // are only expanded when renaming properties in mapped types. + // Returning `never` is fine because the mapped type expansion will + // filter out any keys whose type is `never`. + return + { Kind = TypeKind.Keyword Keyword.Never + Provenance = None } + else + let isLiteralOrIntrinsic t = + match t.Kind with + | TypeKind.Literal _ -> true + | TypeKind.IntrinsicInstance { TypeArgs = typeArgs } -> + match typeArgs with + | Some [ { Kind = TypeKind.Literal _ } ] -> true + | _ -> false + | _ -> false + + // If all `elems` are literals, we can expand the type to a string. + // This is used for property renaming in mapped types. + if List.forall isLiteralOrIntrinsic elems then + let mutable str = "" + + for elem, quasi in + (List.zip elems (List.take elems.Length quasis)) do + match elem.Kind with + | TypeKind.Literal(Literal.String s) -> str <- str + quasi + s + | TypeKind.Literal(Literal.Number n) -> + str <- str + quasi + string (n) + | TypeKind.Literal(Literal.Boolean b) -> + str <- str + quasi + string (b) + | TypeKind.IntrinsicInstance { Name = name + TypeArgs = Some [ { Kind = TypeKind.Literal(Literal.String s) } ] } -> + match name with + | QualifiedIdent.Ident "Uppercase" -> + str <- str + quasi + s.ToUpper() + | QualifiedIdent.Ident "Lowercase" -> + str <- str + quasi + s.ToLower() + | QualifiedIdent.Ident "Capitalize" -> + str <- + str + + quasi + + s.[0].ToString().ToUpper() + + s.[1..].ToLower() + | QualifiedIdent.Ident "Uncapitalize" -> + str <- + str + + quasi + + s.[0].ToString().ToLower() + + s.[1..].ToUpper() + | _ -> + failwith $"Unsupported intrinsic {name} in template literal" + | _ -> () // should never happen because we pre-filtered the list + + str <- str + List.last quasis + + return + { Kind = TypeKind.Literal(Literal.String str) + Provenance = None } + else + return t | _ -> // Replaces type parameters with their corresponding type arguments // TODO: do this more consistently