I won't stress about validating types within the execution of wire()
, as I believe your primary concern lies in the behavior from the point of view of the caller. To address this, the implementation may require some type assertions or similar techniques to handle errors, given the complexity of the type logic involved here. Refer to microsoft/TypeScript#48992 for a proposed feature that could facilitate type checking during implementation.
So, let's outline one possible call signature for wire()
:
declare function wire<T1, T2>(
o1: T1, o2: T2,
k1: KeysMatchingForWrites<T1, T2>,
k2: KeysMatchingForWrites<T2, T1>
): void;
type KeysMatchingForWrites<T, V> =
keyof { [K in keyof T as [V] extends [T[K]] ? K : never]: any };
The
KeysMatchingForWrites<T, V>
type identifies properties of type
T
where a value of type
V</code can be safely written. This is achieved through <a href="https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as" rel="nofollow noreferrer">key remapping</a>, focusing on keys where <code>V
is a subtype of the property type
T[K]
(using the
conditional type [V] extends [T[K]] ? K : never
). Enclosing
V
and
T[K]
within brackets prevents the
distribution of their types.
Let's test its functionality:
wire(animal, person, "owner1", "animal1");
// function wire<Animal, Person>(
// o1: Animal, o2: Person, k1: "owner1" | "owner2", k2: "animal1" | "animal2"
// ): void
Seems promising. The type for k1
is "owner1" | "owner2"
, and for k2
it's
"animal1" | "animal2"</code – exactly as intended. Both <code>numberOfLegs
and
name
are appropriately rejected.
Notably,
KeysMatchingForWrites<T, V>
verifies that
V
serves as a subtype of
T[K]</code, not the other way around. The emphasis lies on safe writing rather than reading. For instance:</p>
<pre><code>interface SuperFoo { u: string; }
interface Foo extends SuperFoo { v: Bar }
interface SubFoo extends Foo { w: string; }
declare const foo: Foo;
interface Bar {
x: SuperFoo, // broader inclusion is acceptable
y: Foo,
z: SubFoo; // narrower inclusion is not permitted
}
declare const bar: Bar;
// function wire<Foo, Bar>(o1: Foo, o2: Bar, k1: "v", k2: "x" | "y"): void
wire(foo, bar, "v", "x"); // valid, assigning Foo to SuperFoo is possible
wire(foo, bar, "v", "y"); // fine, assigning Foo to Foo is acceptable
wire(foo, bar, "v", "z"); // error, cannot necessarily assign Foo to SubFoo
In the above example, you assign foo.v
to the k2
property of bar
. Since foo.v
corresponds to type Foo
, it can only be safely assigned to bar.x
and bar.y
. As bar.y
is of type SuperFoo</code and all <code>Foo
instances are also SuperFoo
, this assignment is allowed. However, trying to write to bar.z
is disallowed since it requires a SubFoo
, which not every Foo
object embodies.
Link to code on Playground