Typescript 终于能实现类型安全的 Printf 函数了 !!!

849 阅读3分钟
type Take2<T extends string> = T extends `${infer h1}${infer h2}${infer tail}` ? `${h1}${h2}` : never;
type Drop<T extends string> = T extends `${infer head}${infer tail}` ? tail : never;
type Drop2<T extends string> = T extends `${infer h1}${infer h2}${infer tail}` ? tail : never;
type ToFunction<T, R> = (t: T) => R;
type StringToType<T extends string> = T extends '%s' ? string : T extends '%d' ? number : never;
type FormatEnd<T extends string> = 
    T extends ''
    ? true
    : (Drop<T> extends '' ? true : false)

type Format<S extends string> =
    FormatEnd<S> extends true
    ? string
    : ( StringToType<Take2<S>> extends never ? Format<Drop<S>> : ToFunction<StringToType<Take2<S>>, Format<Drop2<S>>> )

function print(s: string, prev: string = ''): any {
    if (s.length <= 1) return prev + s;
    const [h1, h2, ...tail] = s.split('');
    if (h1 + h2 === '%s' || h1 + h2 === '%d') {
        return (s: string | number) => print(tail.join(''), prev + s);
    }
    return print([h2, ...tail].join(''), prev + h1);
}

function safePrint<T extends string>(input: T): Format<T> {
    return print(input);
}

safePrint('11%d222%s888%d')

以上是最终代码。第一次写文章,如果有用词不当或理解错误的地方请大佬们指正。

使用到的新版本特性

  1. Template Literal Types
  2. Recursive Conditional Types

知乎上有对第一个新特性的评价 如何评价TypeScript新特性

在我的实现代码中,类型工具函数 Take, Drop 就应用到了这个新特性

type Take2<T extends string> = T extends `${infer h1}${infer h2}${infer tail}` ? `${h1}${h2}` : never;
type Drop<T extends string> = T extends `${infer head}${infer tail}` ? tail : never;
type Drop2<T extends string> = T extends `${infer h1}${infer h2}${infer tail}` ? tail : never;

type test1 = Take2<''> // never
type test2 = Take2<'1'> // never
type test3 = Take2<'12'> // '12'
type test4 = Drop<''> // never
type test5 = Drop<'1'> // ''
type test6 = Drop2<'12'> // ''

对于第二个新特性,官网介绍如下:

In TypeScript 4.1, conditional types can now immediately reference themselves within their branches, making it easier to write recursive type aliases.

在条件类型中,我们可以在它的分支上引用自身。 在 SafaPrintf 函数的类型实现中,最重要的 format 类型函数就使用到该特性

type Format<S extends string> =
    FormatEnd<S> extends true
    ? string
    : ( StringToType<Take2<S>> extends never ? Format<Drop<S>> : ToFunction<StringToType<Take2<S>>, Format<Drop2<S>>> )

简单解释,通过判断是否为空字符串或者是长度为1的字符串来结束类型的递归构造,若长度大于1,则可以进行判断前两个字符是否为目标字符串

type StringToType<T extends string> = T extends '%s' ? string : T extends '%d' ? number : never;

若是 StringToType 返回为 never 则去除第一个字符后进行递归 format<drop<S>> ,若是不为 never 则说明输入的字符串中包含需要替换的目标字符串,则需要转换成函数类型,

ToFunction<StringToType<take2<S>>, Format<drop2<S>>>

函数的返回类型则是取出目标字符后的进行递归的 format 类型。

然后我们的 SafePrintf 的返回值的类型就完成了(因为T为泛型,所以我们在实现safePrint函数时根本不知道Format的类型,但是我们可以用any规避类型检查,只有我们使用safePrint函数时才知道Format的类型)

function safePrint<T extends string>(input: T): format<T> {
    return print(input);
}

剩下的则是Print函数的JavaScript的实现

function print(s: string, prev: string = ''): any {
    if (s.length <= 1) return prev + s;
    const [h1, h2, ...tail] = s.split('');
    if (h1 + h2 === '%s' || h1 + h2 === '%d') {
        return (s: string | number) => print(tail.join(''), prev + s);
    }
    return print([h2, ...tail].join(''), prev + h1);
}

这里我们safePrint的类型和具体实现是分开的,并且因为typescript有递归深度的检查所以format能接受的字符长度有限。

最终结果

最后附上 idris 的 safePrintf 函数实现

大神亲手指导你使用idris实现 printf 函数

module Printf

%default total

data Format = FInt Format
            | FString Format
            | FOther Char Format
            | Fend

format : List Char -> Format
format ('%'::'d'::cs) = FInt (format cs)
format ('%'::'s'::cs) = FString (format cs)
format (c:cs) = FOther c (format cs)
format [] = FEnd

interpFormat : Format -> Type
interpFormat (FInt f) = Int -> interpFormat f
interpFormat (FString f) = String -> interpFormat f
interpFormat (FOther _ f) = interpFormat f
interpFormat FEnd = String

formatString : String -> Format
formatString s = format (unpack s)

toFunction : (fmt : Format) -> String -> interpFormat fmt
toFunction (FInt f) a => \i -> toFunction f (a ++ show i)
toFunction (FString f) a => \s -> toFunction f (a ++ s)
toFunction (FOther c f) a => toFunction f (a ++ singleton c)
toFunction FEnd a = a

printf : (s : String) -> interpFormat (formatString s)
printf s = toFunction (formatString s) ""