Skip to content

Commit

Permalink
Fix the Schema.TemplateLiteral output type when the arguments inclu… (
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti authored Dec 5, 2024
1 parent 3862cd3 commit 90906f7
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 37 deletions.
37 changes: 37 additions & 0 deletions .changeset/empty-tables-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
"effect": patch
---

Fix the `Schema.TemplateLiteral` output type when the arguments include a branded type.

Before

```ts
import { Schema } from "effect"

const schema = Schema.TemplateLiteral(
"a ",
Schema.String.pipe(Schema.brand("MyBrand"))
)

// type Type = `a ${Schema.brand<typeof Schema.String, "MyBrand"> & string}`
// | `a ${Schema.brand<typeof Schema.String, "MyBrand"> & number}`
// | `a ${Schema.brand<typeof Schema.String, "MyBrand"> & bigint}`
// | `a ${Schema.brand<...> & false}`
// | `a ${Schema.brand<...> & true}`
type Type = typeof schema.Type
```
After
```ts
import { Schema } from "effect"

const schema = Schema.TemplateLiteral(
"a ",
Schema.String.pipe(Schema.brand("MyBrand"))
)

// type Type = `a ${string & Brand<"MyBrand">}`
type Type = typeof schema.Type
```
194 changes: 169 additions & 25 deletions packages/effect/dtslint/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,54 @@ S.instanceOf(Test)
// TemplateLiteral
// ---------------------------------------------

// $ExpectType TemplateLiteral<"a">
S.TemplateLiteral("a")

// $ExpectType TemplateLiteral<"a">
S.TemplateLiteral(S.Literal("a"))

// $ExpectType TemplateLiteral<"1">
S.TemplateLiteral(1)

// $ExpectType TemplateLiteral<"1">
S.TemplateLiteral(S.Literal(1))

// $ExpectType TemplateLiteral<string>
S.TemplateLiteral(S.String)

// $ExpectType TemplateLiteral<`${number}`>
S.TemplateLiteral(S.Number)

// $ExpectType TemplateLiteral<"ab">
S.TemplateLiteral("a", "b")

// $ExpectType TemplateLiteral<"ab">
S.TemplateLiteral(S.Literal("a"), S.Literal("b"))

// $ExpectType TemplateLiteral<`a${string}`>
S.TemplateLiteral("a", S.String)

// $ExpectType TemplateLiteral<`a${string}`>
S.TemplateLiteral(S.Literal("a"), S.String)

// $ExpectType TemplateLiteral<`a${number}`>
S.TemplateLiteral("a", S.Number)

// $ExpectType TemplateLiteral<`a${number}`>
S.TemplateLiteral(S.Literal("a"), S.Number)

// $ExpectType TemplateLiteral<`${string}a`>
S.TemplateLiteral(S.String, "a")

// $ExpectType TemplateLiteral<`${string}a`>
S.TemplateLiteral(S.String, S.Literal("a"))

// $ExpectType TemplateLiteral<`${number}a`>
S.TemplateLiteral(S.Number, "a")

// $ExpectType TemplateLiteral<`${number}a`>
S.TemplateLiteral(S.Number, S.Literal("a"))

// $ExpectType TemplateLiteral<`${string}0`>
S.TemplateLiteral(S.String, 0)

Expand All @@ -1183,18 +1231,6 @@ S.TemplateLiteral(S.String, 1n)
// $ExpectType TemplateLiteral<`${string}a` | `${string}0`>
S.TemplateLiteral(S.String, S.Literal("a", 0))

// $ExpectType TemplateLiteral<`a${string}`>
S.TemplateLiteral(S.Literal("a"), S.String)

// $ExpectType TemplateLiteral<`a${string}`>
S.TemplateLiteral("a", S.String)

// $ExpectType TemplateLiteral<`${string}/`>
S.TemplateLiteral(S.String, S.Literal("/"))

// $ExpectType TemplateLiteral<`${string}/`>
S.TemplateLiteral(S.String, "/")

// $ExpectType TemplateLiteral<`${string}/${number}`>
S.TemplateLiteral(S.String, S.Literal("/"), S.Number)

Expand All @@ -1211,6 +1247,29 @@ S.TemplateLiteral(S.Union(EmailLocaleIDs, FooterLocaleIDs), S.Literal("_id"))
// $ExpectType TemplateLiteral<"welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id">
S.TemplateLiteral(S.Union(EmailLocaleIDs, FooterLocaleIDs), "_id")

// Branded type support

// $ExpectType TemplateLiteral<`${string & Brand<"MyBrand">}`>
S.TemplateLiteral(S.String.pipe(S.brand("MyBrand")))

// $ExpectType TemplateLiteral<`${number & Brand<"MyBrand">}`>
S.TemplateLiteral(S.Number.pipe(S.brand("MyBrand")))

// $ExpectType TemplateLiteral<`a${string & Brand<"MyBrand">}`>
S.TemplateLiteral("a", S.String.pipe(S.brand("MyBrand")))

// $ExpectType TemplateLiteral<`a${string & Brand<"MyBrand">}`>
S.TemplateLiteral(S.Literal("a"), S.String.pipe(S.brand("MyBrand")))

// $ExpectType TemplateLiteral<`${"a" & Brand<"L">}${string & Brand<"MyBrand">}`>
S.TemplateLiteral(S.Literal("a").pipe(S.brand("L")), S.String.pipe(S.brand("MyBrand")))

// $ExpectType TemplateLiteral<`a${number & Brand<"MyBrand">}`>
S.TemplateLiteral("a", S.Number.pipe(S.brand("MyBrand")))

// $ExpectType TemplateLiteral<`a${number & Brand<"MyBrand">}`>
S.TemplateLiteral(S.Literal("a"), S.Number.pipe(S.brand("MyBrand")))

// ---------------------------------------------
// attachPropertySignature
// ---------------------------------------------
Expand Down Expand Up @@ -2698,33 +2757,118 @@ S.Array(S.String).pipe(S.minItems(1), S.maxItems(2))
// $ExpectType Schema<readonly ["a"], "a", never>
S.asSchema(S.TemplateLiteralParser("a"))

// $ExpectType TemplateLiteralParser<["a"]>
S.TemplateLiteralParser("a")
// $ExpectType Schema<readonly ["a"], "a", never>
S.asSchema(S.TemplateLiteralParser(S.Literal("a")))

// $ExpectType Schema<readonly [1], "1", never>
S.asSchema(S.TemplateLiteralParser(1))

// $ExpectType Schema<readonly [1], "1", never>
S.asSchema(S.TemplateLiteralParser(S.Literal(1)))

// $ExpectType Schema<readonly [string], string, never>
S.asSchema(S.TemplateLiteralParser(S.String))

// $ExpectType Schema<readonly [number], `${number}`, never>
S.asSchema(S.TemplateLiteralParser(S.Number))

// $ExpectType Schema<readonly ["a", "b"], "ab", never>
S.asSchema(S.TemplateLiteralParser("a", "b"))

// $ExpectType TemplateLiteralParser<["a", "b"]>
S.TemplateLiteralParser("a", "b")
// $ExpectType Schema<readonly ["a", "b"], "ab", never>
S.asSchema(S.TemplateLiteralParser(S.Literal("a"), S.Literal("b")))

// $ExpectType Schema<readonly ["a", string], `a${string}`, never>
S.asSchema(S.TemplateLiteralParser("a", S.String))

// $ExpectType Schema<readonly ["a", string], `a${string}`, never>
S.asSchema(S.TemplateLiteralParser(S.Literal("a"), S.String))

// $ExpectType Schema<readonly ["a", number], `a${number}`, never>
S.asSchema(S.TemplateLiteralParser("a", S.Number))

// $ExpectType Schema<readonly ["a", number], `a${number}`, never>
S.asSchema(S.TemplateLiteralParser(S.Literal("a"), S.Number))

// $ExpectType Schema<readonly [string, "a"], `${string}a`, never>
S.asSchema(S.TemplateLiteralParser(S.String, "a"))

// $ExpectType Schema<readonly [string, "a"], `${string}a`, never>
S.asSchema(S.TemplateLiteralParser(S.String, S.Literal("a")))

// $ExpectType Schema<readonly [number, "a"], `${number}a`, never>
S.asSchema(S.TemplateLiteralParser(S.Int, "a"))
S.asSchema(S.TemplateLiteralParser(S.Number, "a"))

// $ExpectType Schema<readonly [number, "a"], `${number}a`, never>
S.asSchema(S.TemplateLiteralParser(S.Number, S.Literal("a")))

// $ExpectType Schema<readonly [string, 0], `${string}0`, never>
S.asSchema(S.TemplateLiteralParser(S.String, 0))

// $ExpectType Schema<readonly [string, true], `${string}true`, never>
S.asSchema(S.TemplateLiteralParser(S.String, true))

// $ExpectType Schema<readonly [string, null], `${string}null`, never>
S.asSchema(S.TemplateLiteralParser(S.String, null))

// $ExpectType Schema<readonly [string, 1n], `${string}1`, never>
S.asSchema(S.TemplateLiteralParser(S.String, 1n))

// $ExpectType Schema<readonly [string, 0 | "a"], `${string}a` | `${string}0`, never>
S.asSchema(S.TemplateLiteralParser(S.String, S.Literal("a", 0)))

// $ExpectType Schema<readonly [string, "/", number], `${string}/${number}`, never>
S.asSchema(S.TemplateLiteralParser(S.String, S.Literal("/"), S.Number))

// $ExpectType Schema<readonly [string, "/", number], `${string}/${number}`, never>
S.asSchema(S.TemplateLiteralParser(S.String, "/", S.Number))

// example from https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html

// $ExpectType TemplateLiteralParser<[typeof Int, "a"]>
S.TemplateLiteralParser(S.Int, "a")
// $ExpectType Schema<readonly ["welcome_email" | "email_heading" | "footer_title" | "footer_sendoff", "_id"], "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id", never>
S.asSchema(S.TemplateLiteralParser(S.Union(EmailLocaleIDs, FooterLocaleIDs), S.Literal("_id")))

// $TemplateLiteralParser Schema<readonly ["welcome_email" | "email_heading" | "footer_title" | "footer_sendoff", "_id"], "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id", never>
S.asSchema(S.TemplateLiteral(S.Union(EmailLocaleIDs, FooterLocaleIDs), "_id"))

// Branded type support

// $ExpectType Schema<readonly [string & Brand<"MyBrand">], string, never>
S.asSchema(S.TemplateLiteralParser(S.String.pipe(S.brand("MyBrand"))))

// $ExpectType Schema<readonly [number & Brand<"MyBrand">], `${number}`, never>
S.asSchema(S.TemplateLiteralParser(S.Number.pipe(S.brand("MyBrand"))))

// $ExpectType Schema<readonly ["a", string & Brand<"MyBrand">], `a${string}`, never>
S.asSchema(S.TemplateLiteralParser("a", S.String.pipe(S.brand("MyBrand"))))

// $ExpectType Schema<readonly ["a", string & Brand<"MyBrand">], `a${string}`, never>
S.asSchema(S.TemplateLiteralParser(S.Literal("a"), S.String.pipe(S.brand("MyBrand"))))

// $ExpectType Schema<readonly ["a" & Brand<"L">, string & Brand<"MyBrand">], `a${string}`, never>
S.asSchema(S.TemplateLiteralParser(S.Literal("a").pipe(S.brand("L")), S.String.pipe(S.brand("MyBrand"))))

// $ExpectType Schema<readonly ["a", number & Brand<"MyBrand">], `a${number}`, never>
S.asSchema(S.TemplateLiteralParser("a", S.Number.pipe(S.brand("MyBrand"))))

// $ExpectType Schema<readonly ["a", number & Brand<"MyBrand">], `a${number}`, never>
S.asSchema(S.TemplateLiteralParser(S.Literal("a"), S.Number.pipe(S.brand("MyBrand"))))

// $ExpectType Schema<readonly ["a"], "a", never>
S.asSchema(S.TemplateLiteralParser("a"))

// $ExpectType Schema<readonly ["a", "b"], "ab", never>
S.asSchema(S.TemplateLiteralParser("a", "b"))

// $ExpectType Schema<readonly [number, "a"], `${number}a`, never>
S.asSchema(S.TemplateLiteralParser(S.Int, "a"))

// $ExpectType Schema<readonly [number, "a", string], `${string}a${string}`, never>
S.asSchema(S.TemplateLiteralParser(S.NumberFromString, "a", S.NonEmptyString))

// $ExpectType TemplateLiteralParser<[typeof NumberFromString, "a", typeof NonEmptyString]>
S.TemplateLiteralParser(S.NumberFromString, "a", S.NonEmptyString)

// $ExpectType Schema<readonly ["/", number, "/", "a" | "b"], `/${number}/a` | `/${number}/b`, never>
S.asSchema(S.TemplateLiteralParser("/", S.Int, "/", S.Literal("a", "b")))

// $ExpectType TemplateLiteralParser<["/", typeof Int, "/", Literal<["a", "b"]>]>
S.TemplateLiteralParser("/", S.Int, "/", S.Literal("a", "b"))

// ---------------------------------------------
// UndefinedOr
// ---------------------------------------------
Expand Down
30 changes: 18 additions & 12 deletions packages/effect/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -689,9 +689,11 @@ const makeEnumsClass = <A extends EnumsDefinition>(
*/
export const Enums = <A extends EnumsDefinition>(enums: A): Enums<A> => makeEnumsClass(enums)

type Join<Params> = Params extends [infer Head, ...infer Tail] ?
`${(Head extends Schema<infer A> ? A : Head) & (AST.LiteralValue)}${Join<Tail>}`
: ""
type GetTemplateLiteralType<Params> = Params extends [infer Head, ...infer Tail] ?
Head extends AST.LiteralValue ? `${Head}${GetTemplateLiteralType<Tail>}` :
Head extends Schema<infer A extends AST.LiteralValue, infer _I> ? `${A}${GetTemplateLiteralType<Tail>}` :
never :
``

/**
* @category API interface
Expand All @@ -707,7 +709,7 @@ type TemplateLiteralParameter = Schema.AnyNoContext | AST.LiteralValue
*/
export const TemplateLiteral = <Params extends array_.NonEmptyReadonlyArray<TemplateLiteralParameter>>(
...[head, ...tail]: Params
): TemplateLiteral<Join<Params>> => {
): TemplateLiteral<GetTemplateLiteralType<Params>> => {
const spans: Array<AST.TemplateLiteralSpan> = []
let h = ""
let ts = tail
Expand Down Expand Up @@ -754,14 +756,18 @@ export const TemplateLiteral = <Params extends array_.NonEmptyReadonlyArray<Temp

type TemplateLiteralParserParameters = Schema.Any | AST.LiteralValue

type TemplateLiteralParserParametersType<T> = T extends [infer Head, ...infer Tail] ?
readonly [Head extends Schema<infer A, infer _I, infer _R> ? A : Head, ...TemplateLiteralParserParametersType<Tail>]
type GetTemplateLiteralParserType<Params> = Params extends [infer Head, ...infer Tail] ? readonly [
Head extends Schema<infer A, infer _I, infer _R> ? A : Head,
...GetTemplateLiteralParserType<Tail>
]
: []

type TemplateLiteralParserParametersEncoded<T> = T extends [infer Head, ...infer Tail] ? `${
& (Head extends Schema<infer _A, infer I, infer _R> ? I : Head)
& (AST.LiteralValue)}${TemplateLiteralParserParametersEncoded<Tail>}`
: ""
type GetTemplateLiteralParserEncoded<Params> = Params extends [infer Head, ...infer Tail] ?
Head extends AST.LiteralValue ? `${Head}${GetTemplateLiteralParserEncoded<Tail>}` :
Head extends Schema<infer _A, infer I extends AST.LiteralValue, infer _R> ?
`${I}${GetTemplateLiteralParserEncoded<Tail>}` :
never :
``

/**
* @category API interface
Expand All @@ -770,8 +776,8 @@ type TemplateLiteralParserParametersEncoded<T> = T extends [infer Head, ...infer
export interface TemplateLiteralParser<Params extends array_.NonEmptyReadonlyArray<TemplateLiteralParserParameters>>
extends
Schema<
TemplateLiteralParserParametersType<Params>,
TemplateLiteralParserParametersEncoded<Params>,
GetTemplateLiteralParserType<Params>,
GetTemplateLiteralParserEncoded<Params>,
Schema.Context<Params[number]>
>
{
Expand Down

0 comments on commit 90906f7

Please sign in to comment.