Skip to content
Open
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
16 changes: 12 additions & 4 deletions packages/patterns/src/type-from-pattern.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ type TFStructuralPattern<P> =
: P extends readonly [infer H, ...infer T]
? [TypeFromPattern<H>, ...TFTuple<T>]
: P extends CopyRecord<any>
? Simplify<{ [K in keyof P]: TypeFromPattern<P[K]> }>
? // Const type parameters preserve object literals as readonly, but
// TypeFromPattern describes matched values using the existing mutable
// record shape.
Simplify<{ -readonly [K in keyof P]: TypeFromPattern<P[K]> }>
Comment on lines +63 to +66

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Endo patterns only apply to immutable data; shouldn't this force readonly?

Suggested change
? // Const type parameters preserve object literals as readonly, but
// TypeFromPattern describes matched values using the existing mutable
// record shape.
Simplify<{ -readonly [K in keyof P]: TypeFromPattern<P[K]> }>
? // An immutable CopyRecord can be described by a mutable template.
Simplify<{ readonly [K in keyof P]: TypeFromPattern<P[K]> }>

: P;

// ===== Internal helpers =====
Expand Down Expand Up @@ -232,13 +235,18 @@ type TFAnd<T extends readonly any[]> = T extends readonly [infer H, ...infer R]
: TypeFromPattern<E>
: unknown;

/** Infer a split record: required fields + optional fields + rest (index signature). */
/**
* Infer a split record: required fields + optional fields + rest (index
* signature).
* Const type parameters preserve object literals as readonly, but matched
* record values retain the existing mutable shape.
Comment on lines +241 to +242

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Const type parameters preserve object literals as readonly, but matched
* record values retain the existing mutable shape.

*/
type TFSplitRecord<Req, Opt, Rest = never> = Simplify<
(Req extends CopyRecord<any>
? { [K in keyof Req]: TypeFromPattern<Req[K]> }
? { -readonly [K in keyof Req]: TypeFromPattern<Req[K]> }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
? { -readonly [K in keyof Req]: TypeFromPattern<Req[K]> }
? { readonly [K in keyof Req]: TypeFromPattern<Req[K]> }

: {}) &
(Opt extends CopyRecord<any>
? { [K in keyof Opt]?: TypeFromPattern<Opt[K]> }
? { -readonly [K in keyof Opt]?: TypeFromPattern<Opt[K]> }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
? { -readonly [K in keyof Opt]?: TypeFromPattern<Opt[K]> }
? { readonly [K in keyof Opt]?: TypeFromPattern<Opt[K]> }

: {}) &
// When the rest arg is the empty-record pattern `{}`
// (i.e. "refuse unsupported options"), don't emit an index
Expand Down
16 changes: 8 additions & 8 deletions packages/patterns/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,13 +277,13 @@ export type PatternMatchers = {
/**
* Matches against the intersection of all sub-Patterns.
*/
and: <P extends Pattern[]>(...subPatts: P) => MatcherOf<'and', P>;
and: <const P extends Pattern[]>(...subPatts: P) => MatcherOf<'and', P>;

/**
* Matches against the union of all sub-Patterns
* (requiring a successful match against at least one).
*/
or: <P extends Pattern[]>(...subPatts: P) => MatcherOf<'or', P>;
or: <const P extends Pattern[]>(...subPatts: P) => MatcherOf<'or', P>;

/**
* Matches against the negation of the sub-Pattern.
Expand Down Expand Up @@ -562,9 +562,9 @@ export type PatternMatchers = {
* are collected and matched against `rest`.
*/
splitArray: <
Req extends Pattern[] = Pattern[], // widest: any patterns (not [] — that would mean "no required")
Opt extends Pattern[] = [], // narrowest: no optional elements when omitted
Rest extends Pattern = never, // narrowest: no rest matching when omitted
const Req extends Pattern[] = Pattern[], // widest: any patterns (not [] — that would mean "no required")
const Opt extends Pattern[] = [], // narrowest: no optional elements when omitted
const Rest extends Pattern = never, // narrowest: no rest matching when omitted
>(
required: [...Req],
optional?: [...Opt],
Expand All @@ -586,9 +586,9 @@ export type PatternMatchers = {
* but may omit properties that appear on `optional`.
*/
splitRecord: <
Req extends CopyRecord<Pattern> = CopyRecord<Pattern>,
Opt extends CopyRecord<Pattern> = {},
Rest extends Pattern = never,
const Req extends CopyRecord<Pattern> = CopyRecord<Pattern>,
const Opt extends CopyRecord<Pattern> = {},
const Rest extends Pattern = never,
>(
required: Req,
optional?: Opt,
Expand Down
85 changes: 85 additions & 0 deletions packages/patterns/test/types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,20 @@ expectType<null>(null as unknown as TypeFromPattern<null>);

// ===== 4. Combinators: or → union, and → intersection, opt, eref =====

// M.or() preserves literal arguments as a literal union.
{
const p = M.or('start', 'continue', 'abort');
type T = TypeFromPattern<typeof p>;
expectType<'start' | 'continue' | 'abort'>(null as unknown as T);
}

// M.or() also preserves literal discriminants in record patterns.
{
const p = M.or({ mode: 'start' }, { mode: 'continue' });
type T = TypeFromPattern<typeof p>;
expectType<{ mode: 'start' } | { mode: 'continue' }>(null as unknown as T);
}

// M.or() → union
{
const p = M.or(M.string(), M.nat());
Expand All @@ -323,6 +337,13 @@ expectType<null>(null as unknown as TypeFromPattern<null>);
expectType<string & bigint>(null as unknown as T);
}

// M.and() preserves literal fields in intersected record patterns.
{
const p = M.and({ mode: 'start' }, { payload: M.string() });
type T = TypeFromPattern<typeof p>;
expectType<{ mode: 'start' } & { payload: string }>(null as unknown as T);
}

// M.opt() → T | void (void rather than undefined; see TFKindMap comment)
{
const p = M.opt(M.string());
Expand Down Expand Up @@ -383,6 +404,19 @@ expectType<null>(null as unknown as TypeFromPattern<null>);
}>(null as unknown as T);
}

// Literal required and optional fields remain narrow.
{
const p = M.splitRecord(
{ mode: M.or('start', 'continue') },
{ phase: 'ready' },
);
type T = TypeFromPattern<typeof p>;
expectType<{
mode: 'start' | 'continue';
phase?: 'ready' | undefined;
}>(null as unknown as T);
}

// ===== 7. splitArray: required only, required + optional =====

// Required only
Expand All @@ -402,6 +436,28 @@ expectType<null>(null as unknown as TypeFromPattern<null>);
expectType<[string, bigint?, boolean?]>(null as unknown as T);
}

// Literal elements remain narrow while preserving the tuple shape.
{
const p = M.splitArray([{ mode: 'start' }, { mode: 'continue' }], ['done']);
type T = TypeFromPattern<typeof p>;
expectType<[{ mode: 'start' }, { mode: 'continue' }, 'done'?]>(
null as unknown as T,
);
}

// Literal rest patterns remain narrow as well.
{
const p = M.splitArray([], [], { mode: 'rest' });
type T = TypeFromPattern<typeof p>;
expectType<{ mode: 'rest' }[]>(null as unknown as T);
}

{
const p = M.splitRecord({}, {}, { mode: 'rest' });
type T = TypeFromPattern<typeof p>;
expectType<{ [key: string]: { mode: 'rest' } }>(null as unknown as T);
}

// ===== 8. Hint parameters (type narrowing) =====

// M.string<`${bigint}`>() → `${bigint}`
Expand Down Expand Up @@ -555,6 +611,35 @@ expectType<null>(null as unknown as TypeFromPattern<null>);
expectType<{ bar: (arg0: string) => bigint }>(null as unknown as Methods);
}

// A nested interface method guard keeps literal discriminants narrow.
{
type Operation = { mode: 'start' } | { mode: 'continue' | 'abort' | 'skip' };
const OperationShape = M.or(
M.splitRecord({ mode: 'start' }),
M.splitRecord({ mode: M.or('continue', 'abort', 'skip') }),
);
expectType<Operation>(
null as unknown as TypeFromPattern<typeof OperationShape>,
);

const ControllerI = M.interface('Controller', {
handle: M.call(OperationShape).returns(M.boolean()),
});
type ControllerMethods = TypeFromInterfaceGuard<typeof ControllerI>;
expectType<{ handle: (arg0: Operation) => boolean }>(
null as unknown as ControllerMethods,
);

const methods: ControllerMethods = {
handle(operation) {
expectType<Operation>(operation);
return operation.mode === 'start';
},
};
// eslint-disable-next-line no-void
void methods;
}

// Multi-method interface
{
const CounterI = M.interface('Counter', {
Expand Down
Loading