スクリプト言語の定義非公開プレビュー
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!"
スキーマの検証
型に注釈を付けて、フィールドの基本的な検証を行えます。これは、スクリプトの構成スキーマを定義するときに使用します。
文字列の検証
/** @minLength 0 @maxLength 10 */ type Greeting = string;
/** @pattern ^hello world$ */ type Greeting = string;
番号の検証
/** @minimum 0 @maximum 100 */ type Percent = number;
/** @exclusiveMinimum -1 @exclusiveMaximum 101 */ type Percent = number;
配列の検証
/** @minItems 1 @maxItems 5 */ type Names = string[];
/** @uniqueItems */ type Names = string[];
検証のユースケースをサポートできない場合は、検証チェックを関数定義に組み込んで、scripts-preview@stripe.com に問い合わせることをお勧めします。
例
割引率関数
この例は、最大金額までパーセンテージで割引する割引関数を示しています。
/** * 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, RunContext } from '@stripe/scripts'; import { ComputeDiscountsFunction, DiscountCalculation, DiscountableItem, DiscountResult, } from '@stripe/scripts/discounts'; type Configuration = { max_amount: MonetaryAmount; percent: Percent; }; const maxAmountPercentOff: ComputeDiscountsFunction<Configuration> = ( context: RunContext, 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, }, }, }; }; const computeMaxAmountPercentOff: DiscountCalculation<Configuration> = { computeDiscounts: maxAmountPercentOff, }; export default computeMaxAmountPercentOff;
数量ベースの段階割引
次の例は、数量に基づいて異なるレートを適用する割引を示しています。
/** * 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 { RunContext } from '@stripe/scripts'; import type { ComputeDiscountsFunction, DiscountableItem, DiscountResult, DiscountCalculation, } from '@stripe/scripts/discounts'; import type {RunContext} from '@stripe/scripts'; /** * Configuration for the discount calculator function */ export type TieredPercentOffDiscountConfiguration = { currency: string; tier_1_minimum_spend_amount: number; tier_1_discount_percent: number; tier_2_minimum_spend_amount: number; tier_2_discount_percent: number; }; /** * Gives a percentage off based on minimum spend amount. * It is assumed tier1 amount < tier2 amount * * @param {TieredPercentOffDiscountConfiguration} configuration - The config containing tier specifications * @param {DiscountableItem} item - The items to apply discounts to * @returns {DiscountResult} - The discounts applied to the items */ const tieredPercentOffDiscountCalculator: ComputeDiscountsFunction< TieredPercentOffDiscountConfiguration > = ( context: RunContext, configuration: TieredPercentOffDiscountConfiguration, discountable_item: DiscountableItem, ): DiscountResult => { const { currency, tier_1_minimum_spend_amount, tier_1_discount_percent, tier_2_minimum_spend_amount, tier_2_discount_percent, } = configuration; let discountAmount = 0; let discountPercent = 0; const invoiceTotal = discountable_item.gross_amount.amount; if ( discountable_item.gross_amount.currency.toLowerCase().trim() === currency.toLowerCase().trim() ) { // Get discount percent based on gross amount switch (true) { case invoiceTotal >= tier_2_minimum_spend_amount: discountPercent = tier_2_discount_percent; break; case invoiceTotal >= tier_1_minimum_spend_amount: discountPercent = tier_1_discount_percent; break; default: break; } discountAmount = (invoiceTotal * discountPercent) / 100; } return { discount: { amount: { amount: discountAmount, currency: discountable_item.gross_amount.currency, }, }, }; }; const computeTieredPercentOffDiscountCalculator: DiscountCalculation<TieredPercentOffDiscountConfiguration> = { computeDiscounts: tieredPercentOffDiscountCalculator, }; export default computeTieredPercentOffDiscountCalculator;
デバッグのヒント
一般的なエラーパターン
スクリプトが静的分析に失敗した場合は、以下の一般的なエラーが起こってないか確認します。
無限ループ:明確な終了条件がないループ
// 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); }