TypeScriptのConditional Typesでinferを使って足し算を定義する

  • 投稿日:
  • by
  • カテゴリ:

String Literal TypesTemplate Literal Typesを使うとGenericsや関数の引数の文字列から型を作れるという話を聞いたので、勉強も兼ねて足し算を定義してみました。

以下のような感じになります。 `Expr<"1 + 1">` が `"10"` というString Literal Typeになります。2進数の足し算のみに対応していて、未対応の式ではUnrecognizedExpression型になります。3項以上でも計算できます。

type Register<TOTAL, CARRY> = [TOTAL, CARRY];
type AddToRegister<
R extends Register<string, string>,
A extends string,
B extends string
> = A extends `1`
? B extends `1`
? R[1] extends `1`
? Register<`1${R[0]}`, `1`>
: Register<`0${R[0]}`, `1`>
: R[1] extends `1`
? Register<`0${R[0]}`, `1`>
: Register<`1${R[0]}`, ``>
: B extends `1`
? R[1] extends `1`
? Register<`0${R[0]}`, `1`>
: Register<`1${R[0]}`, ``>
: R[1] extends `1`
? Register<`1${R[0]}`, ``>
: Register<`0${R[0]}`, ``>;
type Adder<
A extends string,
B extends string,
R extends Register<string, string>
> = A extends `${infer LeaderA}0`
? B extends `${infer LeaderB}0`
? Adder<LeaderA, LeaderB, AddToRegister<R, ``, ``>>
: B extends `${infer LeaderB}1`
? Adder<LeaderA, LeaderB, AddToRegister<R, ``, `1`>>
: Adder<LeaderA, "", AddToRegister<R, ``, ``>>
: A extends `${infer LeaderA}1`
? B extends `${infer LeaderB}0`
? Adder<LeaderA, LeaderB, AddToRegister<R, `1`, ``>>
: B extends `${infer LeaderB}1`
? Adder<LeaderA, LeaderB, AddToRegister<R, `1`, `1`>>
: Adder<LeaderA, "", AddToRegister<R, `1`, ``>>
: B extends `${infer LeaderB}0`
? Adder<"", LeaderB, AddToRegister<R, ``, ``>>
: B extends `${infer LeaderB}1`
? Adder<"", LeaderB, AddToRegister<R, `1`, ``>>
: `${R[1]}${R[0]}`;
type Add<A extends string, B extends string> = A extends UnrecognizedExpression
? UnrecognizedExpression
: B extends UnrecognizedExpression
? UnrecognizedExpression
: Adder<A, B, Register<``, ``>>;
type UnrecognizedExpression = "UnrecognizedExpression";
type BinaryDigit = "0" | "1";
type isBinaryNumber<D> = D extends `${BinaryDigit}${infer Rest}`
? Rest extends ""
? true
: isBinaryNumber<Rest>
: false;
type Expr<E extends string> = E extends ` ${infer WithoutWhitespace}`
? Expr<WithoutWhitespace>
: E extends `${infer WithoutWhitespace} `
? Expr<WithoutWhitespace>
: isBinaryNumber<E> extends true
? E
: E extends `${infer A}+${infer B}`
? Add<Expr<A>, Expr<B>>
: UnrecognizedExpression;
function equalsTo<T>(value: T) {
console.log(value);
}
equalsTo<Expr<"0 + 0">>("0");
equalsTo<Expr<"0 + 1">>("1");
equalsTo<Expr<"1 + 0">>("1");
equalsTo<Expr<"1 + 1">>("10");
equalsTo<Expr<"11 + 1">>("100");
equalsTo<Expr<"11 + 11">>("110");
equalsTo<Expr<"1100100 + 11101011">>("101001111");
equalsTo<Expr<"1 + 1 + 1">>("11");
equalsTo<Expr<"11 + 11 + 11">>("1001");
equalsTo<Expr<"11 + 11 + 11 + 11">>("1100");
equalsTo<Expr<"11 + 11 + 11 + 11 + 11">>("1111");
equalsTo<Expr<"1 + 2">>("UnrecognizedExpression");
equalsTo<Expr<"1 * 1">>("UnrecognizedExpression");
export {}
view raw expr.ts hosted with ❤ by GitHub

型が補完されるエディタを使っていると、答を自分で打たなくても補完されたりして楽しいです。

これをtscで変換すると以下のようになるのですが、これを見て、TypeScriptの型はほんとに実行時には影響のないものなのだなと実感することもできます。

"use strict";
exports.__esModule = true;
function equalsTo(value) {
console.log(value);
}
equalsTo("0");
equalsTo("1");
equalsTo("1");
equalsTo("10");
equalsTo("100");
equalsTo("110");
equalsTo("101001111");
equalsTo("11");
equalsTo("1001");
equalsTo("1100");
equalsTo("1111");
equalsTo("UnrecognizedExpression");
equalsTo("UnrecognizedExpression");
view raw expr.js hosted with ❤ by GitHub

足し算の定義は業務の役には立たないものの、このあたりを勉強しつつTypeScriptを使っているウェブのフレームワーク(🔥)にAdded type to c.req.param key.というPRを作ってみて、引数から型を生成するようなパターンには可能性があるなと感じたりもしています。

ちなみにTypeScriptではdocument.querySelector(`a`)と指定したときに引数からHTMLAnchorElementが推論されたりして面白いのですが、`div a`や`a[href^="#"]`だとHTMLElementになってしまうというのがあるのですが、以下のように指定すると要素名っぽいものから推論されるようにできます。(面白いけど、やっぱりこれも使う機会はないとは思いますが。)

type MyElementTagNameLookup<T extends string> =
T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] :
T extends keyof SVGElementTagNameMap ? SVGElementTagNameMap[T] :
Element;
type MyElementTagNameMapWithSuffix<T extends string> =
T extends `${infer TagName}[${infer _Attr}`
? MyElementTagNameLookup<TagName>
: T extends `${infer TagName}.${infer _ClassName}`
? MyElementTagNameLookup<TagName>
: MyElementTagNameLookup<T>;
type MyElementTagNameMap<T extends string> =
T extends `${infer _Head} ${infer Tail}`
? MyElementTagNameMap<Tail>
: MyElementTagNameMapWithSuffix<T>;
declare global {
interface ParentNode {
querySelectorAll<T extends string>(
selectors: T
): NodeListOf<MyElementTagNameMap<T>>;
querySelector<T extends string>(
selectors: T
): MyElementTagNameMap<T> | null
}
}
document.querySelectorAll(`a[href^="#"]`).forEach((e) => { alert(e.href) });
document.querySelector(`input[name="nickname"]`)?.value;
export {};