I'd be inclined to stay away from complex constraints like this unless you really need it. My recommendation is to build up the type you're talking about from more primitive pieces, like this:
type Unknown = string | number | boolean | symbol | null | void | object;
type Constraint = Exclude<Unknown, object> | { [k: string]: unknown, x?: string };
That Constraint is just a plain old union, and it more or less represents everything except an object with a non-string value for the x key. Is it perfect? Maybe not, but it is a lot easier to deal with:
declare function foo<T extends Constraint>(a: T): boolean;
class SomeClass<T extends Constraint> { /* ... */ };
foo(undefined); // okay
foo(null); // okay
foo("string"); // okay
foo(123); // okay
foo([]); // okay
foo([123]); // okay
foo([123, { x: "string" }]); // okay
foo(() => 123); // okay
foo({}); // okay
foo({ a: 123 }); // okay
foo({ a: 123, x: 123 }); // error
foo({ a: 123, x: { y: 123 } }); // error
foo(Math.random() < 0.5 ? 1 : { a: 123, x: "string" }); // okay
And you have no problem inside SomeClass anymore:
class SomeClass<T extends Constraint> {
prop!: T;
method() {
foo(this.prop); // easy-peasy
}
}
If you find yourself in dire need of a circular or self-referential constraint, it is possible to placate the compiler, but this tends to be a trial-and-error thing for me with many pitfalls along the way. Let's start with your type function:
type Constrained<T> = 'x' extends keyof T
? (T extends { x: string } ? T : never)
: T;
Your initial foo definition seems to work, but only via some possibly-risky type inference:
declare function foo<T>(a: Constrained<T>): boolean;
How does the compiler know what T is given that a is of type Constrained<T>? It has to treat Constrained<T> as an inference site for T, by seeing through the conditional type in some way. I guess the compiler sees that Constrained<T> is assignable to never | T, which is T, so it infers that T is the same type as a. Anyway, that's fine.
A more "officially supported" way to do this kind of thing is to make a of type T & Constrained<T>, since intersections are known to serve as inference sites. This is really the same as what you put but would make me sleep more soundly at night:
declare function foo<T>(a: T & Constrained<T>): boolean;
As for the class, the thing you really want to do gives you a circular constraint error:
class SomeClass<T extends Constrained<T>> { /* ... * / } // error!
// Type parameter 'T' has a circular constraint.
This can be worked around by adding some dummy type parameters and using a conditional type whose evaluation is deferred until SomeClass is instantiated concretely:
class SomeClass<T extends (U extends any ? Constrained<T> : unknown), U = any> { /* ... * / }
declare const ok: SomeClass<{ a: string }>; // okay
declare const alsoOk: SomeClass<{ x: string }>; // okay
declare const notOk: SomeClass<{ x: number }>; // error, number not a string
The compiler doesn't notice the circularity anymore, but it's still there.
The implementation of the class will still give you errors, precisely because you're delaying the compiler's inspection of the otherwise circular constraint, so it doesn't know what you're doing will be safe:
class SomeClass<T extends (U extends any ? Constrained<T> : unknown), U = any> {
prop!: T;
method() {
foo(this.prop); // still error
}
}
One way to handle that is to make prop of type Constrained<T> instead of T:
class SomeClass<T extends (U extends any ? Constrained<T> : unknown), U = any> {
prop!: Constrained<T>;
method() {
foo(this.prop); // okay now
}
}
But you will still likely run into other such issues elsewhere, and you might eventually have to just go with type assertions to silence the errors:
class SomeClass<T extends (U extends any ? Constrained<T> : unknown), U = any> {
prop!: T;
method() {
foo(this.prop as Constrained<T>); // I know what I'm doing!
}
}
Anyway, you can see what a mess this is. That's why I still recommend the initial plain-old-union solution.
Okay, hope that helps. Good luck!