Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/functional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,3 +738,39 @@ export function validateEqualsReturn<T extends (...args: any[]) => any>(
export function validateEqualsReturn(): never {
NoTransformConfigurationError("functional.validateEqualsReturn");
}

/* -----------------------------------------------------------
MATCH
----------------------------------------------------------- */
/**
* Pattern matching with types.
*
* Creates a pattern matching expression that validates input against TypeScript
* types and executes corresponding handlers. The function is transformed at
* compile-time to generate optimized conditional statements.
*
* The cases object should have keys that correspond to discriminant values or
* type names, and values that are handler functions for those cases.
*
* @template T Union type to match against
* @template R Return type of the matching result
* @param input Value to pattern match
* @param cases Object with handler functions for different cases
* @param otherwise Optional error handler for unmatched cases
* @returns Result of the matched handler or error handler
* @throws {@link TypeGuardError} if no otherwise handler and no match is found
*
* @author Jeongho Nam - https://github.com/samchon
*/
export function match<T, R>(
input: T,
cases: Record<string, (value: any) => R>,
otherwise?: (error: IValidation.IFailure) => R,
): R;

/**
* @internal
*/
export function match(): never {
NoTransformConfigurationError("functional.match");
}
251 changes: 251 additions & 0 deletions src/programmers/MatchProgrammer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import ts from "typescript";

import { ExpressionFactory } from "../factories/ExpressionFactory";
import { MetadataCollection } from "../factories/MetadataCollection";
import { MetadataFactory } from "../factories/MetadataFactory";

import { MetadataObjectType } from "../schemas/metadata/MetadataObjectType";

import { IProgrammerProps } from "../transformers/IProgrammerProps";

export namespace MatchProgrammer {
export interface IProps extends IProgrammerProps {
input: ts.Expression;
cases: ts.Expression;
otherwise?: ts.Expression;
inputType: ts.Type;
}

export const write = (props: IProps): ts.Expression => {
const collection: MetadataCollection = new MetadataCollection();
const result = MetadataFactory.analyze({
options: {
absorb: true,
functional: false,
constant: false,
escape: false,
},
collection,
type: props.inputType,
checker: props.context.checker,
transformer: props.context.transformer,
});

if (!result.success) {
// Return a simple fallback if metadata analysis fails
return generateSimpleMatch(props);
}

const unions = collection.unions();

if (unions.length === 0) {
// Not a union type, generate simple check
return generateSimpleMatch(props);
}

// Generate optimized conditional statements for union types
return generateUnionMatch(props, unions);
};

const generateSimpleMatch = (props: IProps): ts.Expression => {
// For simple types, just try to match the input against any provided cases
return ExpressionFactory.selfCall(
ts.factory.createBlock([
ts.factory.createVariableStatement(
undefined,
ts.factory.createVariableDeclarationList([
ts.factory.createVariableDeclaration(
"input",
undefined,
undefined,
props.input
)
], ts.NodeFlags.Const)
),
ts.factory.createVariableStatement(
undefined,
ts.factory.createVariableDeclarationList([
ts.factory.createVariableDeclaration(
"cases",
undefined,
undefined,
props.cases
)
], ts.NodeFlags.Const)
),
// Try to find a matching case handler
ts.factory.createReturnStatement(
props.otherwise
? ts.factory.createCallExpression(
props.otherwise,
undefined,
[createValidationError("No matching case found")]
)
: ts.factory.createIdentifier("undefined")
)
], true)
);
};

const generateUnionMatch = (
props: IProps,
unions: MetadataObjectType[][]
): ts.Expression => {
return ExpressionFactory.selfCall(
ts.factory.createBlock([
ts.factory.createVariableStatement(
undefined,
ts.factory.createVariableDeclarationList([
ts.factory.createVariableDeclaration(
"input",
undefined,
undefined,
props.input
)
], ts.NodeFlags.Const)
),
ts.factory.createVariableStatement(
undefined,
ts.factory.createVariableDeclarationList([
ts.factory.createVariableDeclaration(
"cases",
undefined,
undefined,
props.cases
)
], ts.NodeFlags.Const)
),
...generateUnionChecks(unions),
// If no case matches, use otherwise handler or throw
...(props.otherwise
? [ts.factory.createReturnStatement(
ts.factory.createCallExpression(
props.otherwise,
undefined,
[createValidationError("No matching case found")]
)
)]
: [ts.factory.createThrowStatement(
ts.factory.createNewExpression(
ts.factory.createIdentifier("Error"),
undefined,
[ts.factory.createStringLiteral("No matching case found")]
)
)]
)
], true)
);
};

const generateUnionChecks = (
unions: MetadataObjectType[][]
): ts.Statement[] => {
const statements: ts.Statement[] = [];

// Generate type checks for each union member
for (const union of unions) {
for (const objectType of union) {
// For now, generate a placeholder check for each object type
statements.push(
ts.factory.createIfStatement(
generateObjectTypeCheck(objectType),
ts.factory.createBlock([
ts.factory.createReturnStatement(
generateCaseCall(objectType)
)
])
)
);
}
}

return statements;
};

const generateObjectTypeCheck = (
object: MetadataObjectType
): ts.Expression => {
// For now, generate a simple discriminant check
// This should be expanded to use proper type checking capabilities
// For discriminated unions, we would check the discriminant property
if (object.properties.length > 0) {
const firstProp = object.properties[0];
if (firstProp && firstProp.key.constants.length > 0) {
const constantValue = firstProp.key.constants[0]?.values[0]?.value;
if (constantValue !== undefined) {
return ts.factory.createStrictEquality(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier("input"),
ts.factory.createIdentifier(String(constantValue))
),
ts.factory.createStringLiteral(String(constantValue))
);
}
}
}

return ts.factory.createTrue(); // Placeholder
};

const generateCaseCall = (object: MetadataObjectType): ts.Expression => {
// Try to find the case based on discriminant value
if (object.properties.length > 0) {
const firstProp = object.properties[0];
if (firstProp && firstProp.key.constants.length > 0) {
const constantValue = firstProp.key.constants[0]?.values[0]?.value;
if (constantValue !== undefined) {
return ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier("cases"),
ts.factory.createIdentifier(String(constantValue))
),
undefined,
[ts.factory.createIdentifier("input")]
);
}
}
}

// Fallback to a default case
return ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier("cases"),
ts.factory.createIdentifier("default")
),
undefined,
[ts.factory.createIdentifier("input")]
);
};

const createValidationError = (message: string): ts.Expression => {
return ts.factory.createObjectLiteralExpression([
ts.factory.createPropertyAssignment(
"success",
ts.factory.createFalse()
),
ts.factory.createPropertyAssignment(
"errors",
ts.factory.createArrayLiteralExpression([
ts.factory.createObjectLiteralExpression([
ts.factory.createPropertyAssignment(
"path",
ts.factory.createStringLiteral("$input")
),
ts.factory.createPropertyAssignment(
"expected",
ts.factory.createStringLiteral("matching case")
),
ts.factory.createPropertyAssignment(
"value",
ts.factory.createIdentifier("input")
),
ts.factory.createPropertyAssignment(
"message",
ts.factory.createStringLiteral(message)
)
])
])
)
]);
};
}
4 changes: 4 additions & 0 deletions src/transformers/CallExpressionTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { FunctionalValidateFunctionProgrammer } from "../programmers/functional/
import { FunctionalValidateParametersProgrammer } from "../programmers/functional/FunctionalValidateParametersProgrammer";
import { FunctionalValidateReturnProgrammer } from "../programmers/functional/FunctionalValidateReturnProgrammer";
import { FunctionalGenericTransformer } from "./features/functional/FunctionalGenericTransformer";
import { FunctionalMatchTransformer } from "./features/functional/FunctionalMatchTransformer";

import { NamingConvention } from "../utils/NamingConvention";

Expand Down Expand Up @@ -362,6 +363,9 @@ const FUNCTORS: Record<string, Record<string, () => Task>> = {
},
programmer: FunctionalValidateReturnProgrammer.write,
}),

// PATTERN MATCHING
match: () => FunctionalMatchTransformer.transform,
},
http: {
// FORM-DATA
Expand Down
40 changes: 40 additions & 0 deletions src/transformers/features/functional/FunctionalMatchTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import ts from "typescript";

import { MatchProgrammer } from "../../../programmers/MatchProgrammer";

import { ITransformProps } from "../../ITransformProps";
import { TransformerError } from "../../TransformerError";

export namespace FunctionalMatchTransformer {
export const transform = (props: ITransformProps): ts.Expression => {
// CHECK PARAMETER COUNT
if (props.expression.arguments.length < 2)
throw new TransformerError({
code: `typia.functional.match`,
message: `at least 2 arguments required: input and cases.`,
});

const input = props.expression.arguments[0]!;
const cases = props.expression.arguments[1]!;
const otherwise = props.expression.arguments[2];

// GET TYPE INFO
const inputType: ts.Type =
props.expression.typeArguments && props.expression.typeArguments[0]
? props.context.checker.getTypeFromTypeNode(
props.expression.typeArguments[0],
)
: props.context.checker.getTypeAtLocation(input);

// Use MatchProgrammer to generate optimized conditional statements
return MatchProgrammer.write({
...props,
input,
cases,
otherwise,
inputType,
type: inputType,
name: undefined,
});
};
}
26 changes: 26 additions & 0 deletions test-match.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import typia from "./src";

// Test types for pattern matching
type Animal =
| { type: 'dog'; breed: string; }
| { type: 'cat'; lives: number; }
| { type: 'bird'; canFly: boolean; };

const animal: Animal = { type: 'dog', breed: 'Golden Retriever' };

// Basic test - this should compile and transform
try {
const result = typia.functional.match(
animal,
{
dog: (dog: { type: 'dog'; breed: string; }) => `Dog of breed: ${dog.breed}`,
cat: (cat: { type: 'cat'; lives: number; }) => `Cat with ${cat.lives} lives`,
bird: (bird: { type: 'bird'; canFly: boolean; }) => `Bird that ${bird.canFly ? 'can' : 'cannot'} fly`,
},
(error) => `No match found: ${JSON.stringify(error)}`,
);

console.log('Match result:', result);
} catch (e) {
console.log('Error during match:', e);
}
Loading