スクリプト言語の定義非公開プレビュー
Stripe Billing で使用されるスクリプト言語の構文とセマンティクスについてご紹介します。
Stripe のスクリプト言語は TypeScript のサブセットです。TypeScript の機能は維持されていますが、操作が一部制限されているなどの重要な違いがあります。スクリプト言語を使用することで、長時間の実行やメモリの過剰消費などといった問題が緩和されます。
構文
基本型とリテラル
この言語では、次のプリミティブ型がサポートされています。
// Numeric values let num: number = 42; let pi: number = 3.14159; // Strings let greeting: string = "Hello, world!"; let multiline: string = `This is a multi-line string`; // Booleans let active: boolean = true; let completed: boolean = false; // Null let empty: null = null;
変数と宣言
let
または const
を使用して変数を宣言します。
// Variable declarations let count: number = 0; let name: string = "John"; // Constants (must be initialized) const PI: number = 3.14159; const API_KEY: string = "abcd1234";
型注釈はオプションですが、指定が推奨されています。
// Type inferred as number let score = 100; // Explicitly typed let highScore: number = 0;
関数とラムダ
複数の方法で関数を定義します。
// Function declaration function add(a: number, b: number): number { return a + b; } // Arrow function (lambda) const multiply = (a: number, b: number): number => { return a * b; }; // Inline arrow function const double = (x: number): number => x * 2;
オブジェクトと配列
TypeScript に似たオブジェクトと配列を定義します。
// Object literal let person = { name: "Alice", age: 30, address: { city: "Wonderland", zipCode: "12345" } }; // Array literal let numbers = [1, 2, 3, 4, 5]; let names: string[] = ["Alice", "Bob", "Charlie"]; // Array spread let moreNumbers = [...numbers, 6, 7, 8, 9, 10]; // Accessing properties let cityName = person.address.city; let firstNumber = numbers[0];
制御フロー
制御フローステートメントには、ループ (if-else、while、for) が含まれます。
// If-else statement if (score > 100) { console.log("High score!"); } else if (score > 50) { console.log("Good score!"); } else { console.log("Try again!"); } // While loop let i = 0; while (i < 5) { console.log(i); i++; } // For loop for (let j = 0; j < 5; j++) { console.log(j); }
break ステートメントは正常に機能します。
// Break in a loop let i = 0; while (true) { if (i >= 5) { break; } i++; }
演算子
サポートされている標準演算子については、以下を参照してください。
// Arithmetic operators let sum = 5 + 3; let difference = 10 - 4; let product = 3 * 7; let quotient = 20 / 4; let remainder = 10 % 3; // Modulo // Comparison operators let equal = (5 == 5); let notEqual = (5 != 3); let greater = (10 > 5); let less = (5 < 10); let greaterOrEqual = (5 >= 5); let lessOrEqual = (4 <= 4); // Logical operators let and = (true && false); // false let or = (true || false); // true let not = !true; // false // Compound assignment let value = 5; value += 3; // value is now 8
インポートとエクスポート
コードをモジュールに整理します。
// Importing modules import { DiscountableItem } from '@stripe/scripts/discounts'; // Export a function export function calculateTotal(items: DiscountableItem[]): number { let sum = 0; for (let i = 0; i < items.length; i++) { sum += items[i].price; } return sum; } // Default export export default function main() { // Main function logic }
サードパーティのライブラリはサポートされていません。
型システム
型注釈
コロンの後に型を指定します。
let count: number = 0; let name: string = "John"; let active: boolean = true; let items: string[] = ["apple", "banana"];
インターフェイスとオブジェクトタイプ
オブジェクトタイプをインラインで定義するか、インターフェイスとして定義します。
// Inline object type let person: { name: string; age: number } = { name: "Alice", age: 30 }; // Interface definition interface Product { id: string; name: string; price: number; inStock?: boolean; // Optional property } // Using the interface let laptop: Product = { id: "lt-001", name: "Laptop", price: 999.99, inStock: true };
ユニオン型
ユニオン型では、変数に複数の型を持たせることができます。
// Union type let id: string | number; id = "abc123"; // Valid id = 123; // Also valid
型宣言
型エイリアスを作成します。
// Type alias type ID = string | number; // Using the type alias let userId: ID = "user123"; let productId: ID = 456; // Complex type declaration type ApiResponse = { status: number; data: { items: any[]; count: number; }; error?: string; };
静的解析
当社のスクリプト言語には、スクリプトの信頼性と効率性を確認するための静的分析が含まれています。
終端解析
主な機能は終了チェックで、スクリプトを Stripe のインフラで実行すると必ず終了することを保証し、無限ループや再帰を防ぎます。すべての終了コードが終了することを証明できるわけではないため、一部の有効なプログラムを拒否します。以下は、終了スクリプトを記述するためのヒントです。
アナライザーはカラーリングシステムを使用します。
- **T (終端):**終了することが証明されているコード。
- **U (不明):**終了しない可能性のあるコード。
// Guaranteed to terminate - marked as T function countdown(n: number): void { while (n > 0) { n = n - 1; // Decreasing counter } } // Not guaranteed to terminate - marked as U function infinite(): void { while (true) { console.log("This runs forever"); } }
明確な再帰
アナライザーは、再帰関数が減少法を使用するかどうかを確認します。
// Safe recursion - marked as T function factorial(n: number): number { if (n <= 1) return 1; return n * factorial(n - 1); // n decreases with each call } // Unsafe recursion - marked as U function badRecursion(n: number): number { return badRecursion(n + 1); // n increases, no termination }
一般的な静的解析パターン
有限境界を持つ for ループは安全です。
// Safe loop pattern - marked as T for (let i = 0; i < array.length; i++) { // Loop body with terminating operations }
while ループには減少カウンターが必要です。
// Safe while loop - marked as T let counter = 10; while (counter > 0) { // Do something counter--; // Counter decreases }
終端セーフコードの記述
次のガイドラインに従って、終端チェックに合格するコードを作成します。
- 明確な有限境界を持つ for ループを使用する
- while ループに減少カウンターがあること、または最終的に
false
になる条件であることを確認する - 再帰関数に基本ケースと減少引数を含める
- 複雑なループ条件、相互再帰、関数とループの深い入れ子を避ける
// Good pattern for loops function processItems(items: any[]): void { for (let i = 0; i < items.length; i++) { processItem(items[i]); } }
ランタイム環境
スクリプト言語には、スクリプトで使用できる組み込みオブジェクトが用意されています。
組み込みオブジェクト
いくつかの組み込みオブジェクトを使用できます。
Math オブジェクト
// Math operations let min = Math.min(5, 3, 7); // 3 let max = Math.max(5, 3, 7); // 7 let floor = Math.floor(3.7); // 3 let ceil = Math.ceil(3.2); // 4
配列
// Array creation let numbers = [1, 2, 3, 4, 5]; // Array properties let length = numbers.length; // 5 // Array indexing let firstItem = numbers[0]; // 1 let lastItem = numbers[numbers.length - 1]; // 5 // Array methods let sorted = numbers.sort((a, b) => a - b);
文字列操作
// String concatenation let firstName = "John"; let lastName = "Doe"; let fullName = firstName + " " + lastName; // "John Doe" // String with template literals let greeting = `Hello, ${firstName}!`; // "Hello, John!"
例
割引率関数
この例は、最大金額までパーセンテージで割引する割引関数を示しています。
/** * Max Amount Percent Off Discount * * This discount function applies a percentage discount to the gross amount, * but caps the total discount at a maximum amount. It calculates the discount * as a percentage of the gross amount and then ensures it doesn't exceed the * configured maximum. * * Configuration: * - max_amount: The maximum monetary amount that can be discounted * - percent: The percentage discount to apply */ import { MonetaryAmount, Percent } from '@stripe/scripts'; import { DiscountFunction, DiscountableItem, DiscountResult, } from '@stripe/scripts/discounts'; type Configuration = { max_amount: MonetaryAmount; percent: Percent; }; const maxAmountPercentOff: DiscountFunction<Configuration> = ( configuration: Configuration, item: DiscountableItem, ): DiscountResult => { const { max_amount, percent } = configuration; const discount_amount = Math.min(max_amount.amount, item.gross_amount.amount * (percent / 100)); return { discount: { amount: { amount: discount_amount, currency: item.gross_amount.currency, }, status: discount_amount > 0 ? 'APPLIED' : 'NOT_APPLIED', reason: discount_amount > 0 ? 'Discount applied' : 'No discount applied', }, line_item_discounts: [], }; }; export default maxAmountPercentOff;
数量ベースの段階割引
次の例は、数量に基づいて異なるレートを適用する割引を示しています。
/** * Tiered Discount * * This discount function applies percentage discounts based on quantity tiers. * It sums the quantities across all line items and applies the discount percentage * from the highest applicable tier. The discount is calculated on each line item's * subtotal amount. * * Configuration: * - tiers: Array of objects with minimum_quantity and discount_percent * The tiers are sorted by minimum_quantity in descending order to find * the highest applicable tier. */ import { DiscountFunction, DiscountableItem, DiscountResult, ItemDiscount, } from '@stripe/scripts/discounts'; type Configuration = { tiers: Array<{ minimum_quantity: number; discount_percent: number; }>; }; const tieredDiscount: DiscountFunction<Configuration> = ( configuration: Configuration, item: DiscountableItem, ): DiscountResult => { let discountPercent = 0; let totalQuantity = 0; for (let i = 0; i < item.line_items.length; i++) { totalQuantity += item.line_items[i]?.quantity ?? 0; } const sortedTiers = [...configuration.tiers].sort( (a, b) => b.minimum_quantity - a.minimum_quantity, ); for (let i = 0; i < sortedTiers.length; i++) { const tier = sortedTiers[i]; if (totalQuantity >= tier.minimum_quantity) { discountPercent = tier.discount_percent; break; // Stop after finding the first applicable tier } } let totalDiscountAmount = 0; const lineItemDiscounts: ItemDiscount[] = []; for (let i = 0; i < item.line_items.length; i++) { const lineItem = item.line_items[i]; const lineItemDiscount = lineItem.subtotal.amount * (discountPercent / 100); totalDiscountAmount += lineItemDiscount; lineItemDiscounts.push({ discountable_item_id: lineItem.id, discount: { amount: { amount: lineItemDiscount, currency: lineItem.subtotal.currency, }, status: lineItemDiscount > 0 ? 'APPLIED' : 'NOT_APPLIED', reason: lineItemDiscount > 0 ? `${discountPercent}% tier discount applied` : 'No discount applied', }, }); } return { discount: { amount: { amount: totalDiscountAmount, currency: item.gross_amount.currency, }, status: totalDiscountAmount > 0 ? 'APPLIED' : 'NOT_APPLIED', reason: totalDiscountAmount > 0 ? `${discountPercent}% tier discount applied` : 'No discount applied', }, line_item_discounts: lineItemDiscounts, }; }; export default tieredDiscount;
デバッグのヒント
一般的なエラーパターン
スクリプトが静的分析に失敗した場合は、以下の一般的なエラーが起こってないか確認します。
無限ループ:明確な終了条件がないループ
// Problem: No clear exit condition while (x > 0) { doSomething(); // x never changes } // Fix: Add a decreasing counter while (x > 0) { doSomething(); x--; }
非終端再帰:減少法を使用しない再帰呼び出し
// Problem: No decreasing measure function process(data: any): void { process(transformData(data)); } // Fix: Add a depth limit and decreasing measure function process(data: any, depth: number = 10): void { if (depth <= 0) return; process(transformData(data), depth - 1); }