💻
TypeScriptの型システムに四則演算を実装する
はじめに
この記事で紹介するコードは、個人開発のライブラリから引用しています。
下準備
数値をそのまま演算できないので、数値を配列に変換してから配列の長さを取得することで数値を表現します。
// 配列の長さを取得する型
export type Length<T extends unknown[]> = T extends { length: infer L }
? L extends number
? L
: never
: never;
// 数値を配列に変換する
export type NumToArr<
T extends number,
U extends number[] = [],
> = U["length"] extends T ? U : NumToArr<T, [...U, U["length"]]>;
引き算を実装する
配列の差分を取ることで引き算を実装します。
負の数を表現するために文字列に変換してから数値に戻します。
// 引き算
export type Subtract<
A extends number,
B extends number,
> = `${A}` extends `-${infer N extends number}`
? `${B}` extends `-${infer M extends number}`
? Subtract<M, N>
: `-${Add<N, B>}` extends `${infer R extends number}`
? R
: never
: `${B}` extends `-${infer M extends number}`
? Length<[...NumToArr<A>, ...NumToArr<M>]>
: NumToArr<A> extends [...NumToArr<B>, ...infer R]
? Length<R>
: NumToArr<B> extends [...NumToArr<A>, ...infer R]
? `-${Length<R>}` extends `${infer M extends number}`
? M
: never
: never;
type SubTest = Subtract<10, 5>; // 5
type SubTest2 = Subtract<5, 10>; // -5
type SubTest3 = Subtract<10, -5>; // 15
type SubTest4 = Subtract<-5, 10>; // -15
type SubTest5 = Subtract<-5, -10>; // 5
足し算を実装する
マイナスの値は先ほどの引き算型に演算を任せます。
配列を結合してから長さを取得することで足し算を実装します。
// たし算
export type Add<
X extends number,
Y extends number,
> = `${X}` extends `-${infer N extends number}`
? `${Y}` extends `-${infer M extends number}`
? `-${Add<N, M>}` extends `${infer R extends number}`
? R
: never
: Subtract<Y, N>
: `${Y}` extends `-${infer M extends number}`
? Subtract<X, M>
: Length<[...NumToArr<X>, ...NumToArr<Y>]>;
type AddTest = Add<10, 5>; // 15
type AddTest2 = Add<5, 10>; // 15
type AddTest3 = Add<10, -5>; // 5
type AddTest4 = Add<-5, 10>; // 5
掛け算を実装する
足し算を繰り返すことで掛け算を実装します。
掛け算はXをN回足し算すると解釈できます。
1 * 3 = 1 + 1 + 1
// 掛け算
export type Multiply<A extends number, B extends number> = MultiHelper<A, B>;
// 掛け算のヘルパー
type MultiHelper<
X extends number,
Y extends number,
A extends number = 0,
> = `${X}` extends `-${infer N extends number}`
? `${Y}` extends `-${infer M extends number}`
? MultiHelper<N, M, A>
: `-${MultiHelper<N, Y, A>}` extends `${infer R extends number}`
? R
: never
: `${Y}` extends `-${infer M extends number}`
? `-${MultiHelper<X, M, A>}` extends `${infer R extends number}`
? R
: never
: Y extends 0
? A
: MultiHelper<X, Subtract<Y, 1>, Add<X, A>>;
type MultiTest = Multiply<10, 5>; // 50
type MultiTest2 = Multiply<5, 10>; // 50
type MultiTest3 = Multiply<10, -5>; // -50
type MultiTest4 = Multiply<-5, 10>; // -50
type MultiTest5 = Multiply<-5, -10>; // 50
割り算を実装する
引き算を繰り返すことで割り算を実装します。
割り算はXからYを引き算することを何回できるかを判定することで実装できます。
この型ではあまりを切り捨てています。
6 / 3 = 6 - 3 - 3
// 割り算
export type Divide<A extends number, B extends number> = A extends 0
? never
: B extends 0
? never
: `${A}` extends `-${infer N extends number}`
? `${B}` extends `-${infer M extends number}`
? DivideHelper<N, M>
: `-${DivideHelper<N, B>}` extends `${infer R extends number}`
? R
: never
: `${B}` extends `-${infer M extends number}`
? `-${DivideHelper<A, M>}` extends `${infer R extends number}`
? R
: never
: DivideHelper<A, B>;
// 割り算のヘルパー
type DivideHelper<
X extends number,
Y extends number,
A extends number = 0,
> = BGreaterThanA<X, Y> extends true
? A
: DivideHelper<Subtract<X, Y>, Y, Add<A, 1>>;
type DivideTest = Divide<10, 5>; // 2
type DivideTest2 = Divide<5, 10>; // 0
type DivideTest3 = Divide<10, -5>; // -2
type DivideTest4 = Divide<-5, 10>; // 0
type DivideTest5 = Divide<-5, -10>; // 0
あまりを取得する
XからYを引き算することをN回繰り返し、XがYより小さくなったらXを返します。
6 % 3 = 6 - 3 - 3 = 0
// XとYが等しいかどうかを判定する型
export type Equal<X, Y> = X extends Y ? true : false;
// AまたはBが0かどうか
export type ZeroAorB<A extends number, B extends number> = Equal<
A,
0
> extends true
? true
: Equal<B, 0> extends true
? true
: false;
// 比較 (A < B)
export type BGreaterThanA<
A extends number,
B extends number,
> = `${A}` extends `-${infer N extends number}`
? `${B}` extends `-${infer M extends number}`
? BGreaterThanA<N, M>
: BGreaterThanA<N, B>
: `${B}` extends `-${infer M extends number}`
? BGreaterThanA<A, M>
: ZeroAorB<A, B> extends true
? Equal<A, B> extends true
? false
: A extends 0
? true
: false
: BGreaterThanA<Subtract<A, 1>, Subtract<B, 1>>;
// 余り
export type Modulo<
A extends number,
B extends number,
> = `${A}` extends `-${infer N extends number}`
? `${B}` extends `-${infer M extends number}`
? Modulo<N, M>
: Modulo<N, B>
: `${B}` extends `-${infer M extends number}`
? Modulo<A, M>
: BGreaterThanA<A, B> extends true
? A
: Modulo<Subtract<A, B>, B>;
できた型で遊ぶ
1日1歩、3日で3歩、3歩進んで2歩下がるとXに辿り着くまでに何日かかるか計算してみます。
3歩進んだ場合は、翌日2歩下がってスタートするルールです。
import { Add, Multiply, Subtract } from "./mathType";
export type ThreeStepsForwardTwoStepsBack<T extends number> = T extends 1
? 1
: T extends 2
? 2
: T extends 3
? 3
: Add<3, Multiply<3, Subtract<T, 3>>>;
type Test = ThreeStepsForwardTwoStepsBack<1>; // 1
type Test2 = ThreeStepsForwardTwoStepsBack<2>; // 2
type Test3 = ThreeStepsForwardTwoStepsBack<3>; // 3
type Test4 = ThreeStepsForwardTwoStepsBack<4>; // 6
type Test5 = ThreeStepsForwardTwoStepsBack<5>; // 9
type Test6 = ThreeStepsForwardTwoStepsBack<100>; // 294
まとめ
TypeScript上に四則演算を実装できました。
最近シェル芸ならぬ型芸にハマっています。
小数点を扱えないので、扱えるようにするのが次の課題です。
GitHubで編集を提案
Discussion