-
-
Notifications
You must be signed in to change notification settings - Fork 349
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Question: How to use TypeScript interfaces with matchers #1054
Comments
Firstly apologies for this, these types probably shouldn't have been released in that state, which is my fault. It's one of the last things I did as a maintainer -I bought them in as an experiment on the beta branch, and I forgot to highlight this with anyone before I stepped down. My failure to communicate this meant that the current maintainers (quite reasonably) didn't know about the potential problems with this type when 10.x was released, and I forgot all about it. (As an aside, I can't reproduce this with Pact 10.4.1 and Typescript 4.9.4 - so changing the typescript version to match might fix it. However, the problem is in Pact, not in your code) I've ended up writing a long answer - the tl;dr is: tl;drAs a workaround, don't use Note that the
is the same as
Read on for all the gory details. ExplanationThe problem stems from trying to bring in a type to represent arbitrary JSON (the motivation was to avoid mistakes like people putting functions or The problem is that typescript thinks (correctly) that the narrower Why did I think forcing arbitrarily indexable objects was an acceptable restriction? Well, I reasoned that the type on the wire (what you're defining in the Unfortunately, this reasoning (although technically correct) is bad - even though the JSON body isn't technically a Your example is a good practice, and these misguided types are preventing you from doing it. WorkaroundsIt will work if you use Here are two workarounds that don't require you to change your type system. They also blind typescript to the problem, but still let you benefit from compile errors if your Pact expectation deviates from
I don't think it matters which you choose. Here's both options:
Long term fix in PactWhy doesn't
But it should have been:
If these template types live on, this type should be corrected. But really, the right fix is to remove all the These types were a mistake, and I'm very sorry. @mefellows, let me know if you would like a PR that corrects this. |
Hello @TimothyJones, thank you very much for your detailed answer! There are a couple of follow-up questions that came up while trying your suggestions. Nested objectsThe interaction I want to specify should return an object that contains other objects. This seems to be the reason your suggestions do not work. On top of that, the interaction should return an array of such objects. interface Foo {
a: string;
}
const f: Foo = { a: 'working example' };
Matchers.like({ ...f });
Matchers.like(Object.freeze(f));
Matchers.atLeastLike({ ...f }, 1);
Matchers.atLeastLike(Object.freeze(f), 1);
interface Room {
id: string,
foo: Foo
}
const r: Room = { id: 'some guid', foo: { a: "example" }};
Matchers.like({ ...r });
// Argument of type '{ id: string; foo: Foo; }' is not assignable to parameter of type 'AnyTemplate'.
// Type '{ id: string; foo: Foo; }' is not assignable to type 'null'.
Matchers.like(Object.freeze(r));
// Argument of type 'Readonly<Room>' is not assignable to parameter of type 'AnyTemplate'.
// Type 'Readonly<Room>' is not assignable to type 'TemplateMap'.
// Property 'foo' is incompatible with index signature.
// Type 'Foo' is not assignable to type 'string | number | boolean | JsonArray | JsonMap | TemplateMap | TemplateArray | Matcher<unknown> | null'.
// Type 'Foo' is not assignable to type 'TemplateMap'.
// Index signature for type 'string' is missing in type 'Foo'.
Matchers.atLeastLike({ ...r }, 1);
// Argument of type '{ id: string; foo: Foo; }' is not assignable to parameter of type 'AnyTemplate'.
// Type '{ id: string; foo: Foo; }' is not assignable to type 'null'.
Matchers.atLeastLike(Object.freeze(r), 1);
// Argument of type 'Readonly<Room>' is not assignable to parameter of type 'AnyTemplate'. How to use anything beyond
|
Just found a suggestion on Stack Overflow: https://stackoverflow.com/a/67406755/149264 Adjusted to the current Pact version (where type WrappedPact<T> = T extends object
? {
[K in keyof T]: WrappedPact<T[K]>;
}
: T | Matchers.Matcher<T>;
interface Foo {
a: string;
}
const f: WrappedPact<Foo> = { a: 'working example' };
Matchers.like(f);
Matchers.atLeastLike(f, 1);
interface Room {
id: string;
foo: Foo;
}
const r: WrappedPact<Room> = { id: 'some guid', foo: { a: 'example' } };
Matchers.like(r);
Matchers.atLeastLike(r, 1); |
Even nested specifications work! const r: WrappedPact<Room> = { id: Matchers.uuid(), foo: { a: Matchers.regex(/some/, 'example') } }; I think something like this should be included in the distribution. |
Thanks for the update and extra cases. I'll take a look at a PR to fix all this up first thing this week. The nested example unfortunately is expected with that workaround - you have to spread or freeze each object , like It looks like |
…as the side effect that functions, Dates and other inappropriate types can now be passed to matchers, and the benefit that people using interfaces don't get spurious errors. Fixes pact-foundation#1054
I've raised a PR that fixes this by removing the restriction that V3 matchers must be passed |
@agross , @TimothyJones I have trouble here with Given an interface like
and the value such as
or with an example value I get a typescript error
Why does |
Where is the |
It's the same as |
Ah, I see! So, that type shouldn't be necessary after 11.x was released. But, if you want to use it, you can correct the definition like so:
For the explanation, have a read of the Typescript documentation on distributive conditional types. I'm not even certain that the corrected version works for all use cases - but it will at least work for the case in your comment. |
@TimothyJones yes now in v11 everything works without that wrapper interface. Thank you! |
I extended your solution to support |
I think that will fail with empty arrays - those shouldn't be wrapped in For empty arrays, I think the core of the problem is that typescript isn't designed to have a type for "everything that could be assigned as json data" - and what we want here is to extend that (already uncomfortable) type with an additional constraint. For specific projects, a custom wrapper like this might be ok - because you can get close to a type for arbitrary json if you're able to constrain how the type is used. But, I don't think the library can safely expose a type like this to all users, as there will always be edge cases. If you really need it, I'd recommend something like the approach used in Is there a reason you need this type in a recent version of pact? |
You're absolutely right that this can quickly become quite complex. In my case, whenever I encounter arrays in an interaction, I always use eachLike. This keeps things manageable in the short term. The primary reason I’m typing these interactions is to ensure they conform to my DTOs. This helps catch issues like typos, missing properties, or mismatches early in the process. Here’s a quick example export function wrapResponse<T>(data: PactMatchable<T>) {
return { data };
}
pactProvider
.given(Given.POSTS_EXIST)
.uponReceiving('a request for a dummy entity')
.withRequest({
method: 'GET',
path: `/${LATEST_API_VERSION}/posts`,
headers: { ...sharedHeaders },
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: wrapResponse<PostsResponseDTO>(
MatchersV3.eachLike({
id: MatchersV3.string(MOCK_ID),
title: MatchersV3.string(MOCK_TITLE),
slug: MatchersV3.string(MOCK_SLUG),
startDate: MatchersV3.timestamp("yyyy-MM-dd'T'HH:mm:ssX", MOCK_STARTDATE),
// ... rest properties, support for primitives, nested objects, arrays
}),
),
}); |
Hello,
this is a question rather than a bug report or feature request.
I'm quite new to Pact and I would like to reuse the TypeScript interfaces my frontend app uses for the consumer-side of things.
The documentation about matching states that in order to use TypeScript interfaces inside e.g.
willRespondWith/body
you need to wrap them inInterfaceToTemplate
or usetype
instead of interface.I've tried both but the results where not quite satisfactory.
As far as I understand it would be beneficial to use existing
type
s andinterface
s to prevent typos and to define the correct expected value types for the mock server.On the other hand,
Matcher.<something>
seems incompatible with the types expected by interface members. The example from the docs only uses thelike
matcher around the interface, but not for its members.My question is how I would be able to achieve something like this with Pact:
The only thing that comes to mind is to slap
as unknown as string
after the matcher.Software versions
Pact JS 10.4.1
v16.19.0
The text was updated successfully, but these errors were encountered: