gcanti / io-ts

Runtime type system for IO decoding/encoding
https://gcanti.github.io/io-ts/
MIT License
6.7k stars 328 forks source link

Support TS 4.1 Template Literal Types #533

Open egeozcan opened 3 years ago

egeozcan commented 3 years ago

🚀 Feature request

TypeScript introduced Template Literal Types in version 4.1: https://devblogs.microsoft.com/typescript/announcing-typescript-4-1/#template-literal-types

image

https://www.typescriptlang.org/play?ts=4.1.0-pr-40336-88#code/JYOwLgpgTgZghgYwgAgILIN4ChnIMoD2AthACJyQBcy5kA3DvsRAJLjUByArkQEbQMAvlixgAngAcUAMWAAbSFAAqAHiUAaZEoAKUAhOQQAHpBAATAM7IA1hDEEYWgHzIAvJkYBtW2OShkAAYAJBgA5ACyoKHIAD7IEXBG0XGhoYIhFmBQoADmyABkWrr6grIK0AEAutRKnjp6EpVCIi0i4lLI0m6d8oqqqJqhhCS0EKFODK1TIkA

Current Behavior

There's no way (AFAIK) to use template literal types with io-ts.

Desired Behavior

I can replicate Template Literal Types with io-ts.

Suggested Solution

A function like t.templateLiteral(``) that replicates the behavior.

Who does this impact? Who is this for?

People generating types from types.

Describe alternatives you've considered

I'm currently using a script to generate these types. They have to be known at compile time anyway.

Your environment

Software Version(s)
io-ts 2.2.12
fp-ts 2.8.6
TypeScript 4.1.2
egeozcan commented 3 years ago

@gcanti wouldn't a function like t.templateLiteral(``) solve this problem? I'm answering to your tag addition.

An example: https://github.com/colinhacks/zod/issues/419

andrevmatos commented 3 years ago

Template Literals could be modelled as a refinement of the string codec. Unfortunately, there seems to be no way to let TypeScript knows about the relationship between a template string or regex at runtime and the template literal type at compile time, so some duplication is inevitable when declaring the codec, but at least the usage would be coherent and correct from there. A proposal:

import * as t from 'io-ts';

export function templateLiteral<L extends string>(regex: RegExp | string, name?: string) {
  const regex_ = typeof regex === 'string' ? new RegExp(regex) : regex;
  const predicate = (s: string): s is L => regex_.test(s);
  return new t.RefinementType(
    name ?? `TemplateLiteral<${regex_.source}>`,
    (u): u is L => t.string.is(u) && predicate(u),
    (i, c) => {
      const e = t.string.validate(i, c);
      if (isLeft(e)) {
        return e;
      }
      const a = e.right;
      return predicate(a) ? t.success(a) : t.failure(a, c);
    },
    t.string.encode,
    t.string,
    predicate,
  );
}

// Usage
const hexString = templateLiteral<`0x{string}`>(/^0x.*/);
declare const str: unknown;
if (hexString.is(str)) {
  // type of str: `0x{string}`
}
jppellet commented 1 year ago

Thanks for the nice example!

Spotted a typo, missing $: it should probably be

const hexString = templateLiteral<`0x${string}`>(/^0x.*/);
                                     ^