2

I have a function that accepts an object of keys and each value has a type such that for each one the type of one of its fields determine the type for the other field. Code:

// We have this Alpha type and echo function...

type NoInfer<T> = [T][T extends unknown ? 0 : never]

interface Alpha<Foo extends string> {
  foo: Foo
  bar: `Depends on ${NoInfer<Foo>}`
}

declare const echo: <T extends string>(x: Alpha<T>) => void

echo({ foo: 'beta', bar: 'Depends on beta'})

// @ts-expect-error Trailing 2 is wrong
echo({ foo: 'beta', bar: 'Depends on beta 2'})

// Now we want a function (bravo) that takes a keyed index of Alphas...

declare const bravo: <T extends { [k: string]: Alpha<string> }>(xs: T) => void

bravo({
  one:  { foo: `1`,  bar: `Depends on 1` },
  // @ts-expect-error 1 !== 1x           <-- fails
  oneX: { foo: `1x`, bar: `Depends on 1` },
  two:  { foo: `2`,  bar: `Depends on 2` },
  // @ts-expect-error 2 !== 2x           <-- fails
  twoX: { foo: `2x`, bar: `Depends on 2` },
})

// how could this work?

playground link

As you can see from the "fails" comments I can make Alpha work initially but in the more complex object of Alphas I fail. Can you help me figure this out? Thanks!

Jason Kuhrt
  • 736
  • 1
  • 4
  • 12

1 Answers1

1

You can write this so that T is an object type whose properties are the strings you pass as type arguments to Alpha, and then make xs a mapped type over T, like this:

declare const bravo: <T extends { [K in keyof T]: string }>(
  xs: { [K in keyof T]: Alpha<T[K]> }
) => void

Note that the recursive constraint { [K in keyof T]: string } is used to guarantee that every property of T is string without using the index signature { [k: string]: string } which would reject interface types without index signatures (see microsoft/TypeScript#15300 and How to constrain a TypeScript interface to have only string property values? for more info).

Anyway, because the type of xs is a homomorphic mapped type (see What does "homomorphic mapped type" mean?), then the compiler can infer T from it when you call the function (this used to be documented but the new handbook doesn't seem to mention it ‍♂️). Let's test it out:

bravo({
  one: { foo: `1`, bar: `Depends on 1` },  // okay
  oneX: { foo: `1x`, bar: `Depends on 1` }, // error
  // --------------> ~~~
  // Type '"Depends on 1"' is not assignable to type '"Depends on 1x"'
  two: { foo: `2`, bar: `Depends on 2` }, // okay
  twoX: { foo: `2x`, bar: `Depends on 2` }, // error
  // --------------> ~~~
  // Type '"Depends on 2"' is not assignable to type '"Depends on 2x"'
})

Looks good. If you hover over that function call in an IntelliSense-enabled IDE you'll get the Quick Info

/* const bravo: <{
    one: "1";
    oneX: "1x";
    two: "2";
    twoX: "2x";
}>(xs: {
    one: Alpha<"1">;
    oneX: Alpha<"1x">;
    two: Alpha<"2">;
    twoX: Alpha<"2x">;
}) => void */

showing that T is inferred as {one: "1", oneX: "1x", two: "2", twoX: "2x"}, and therefore xs's type is checked against {one: Alpha<"1">, oneX: Alpha<"1x">, two: Alpha<"2">, twoX: Alpha<"2x">}, which succeeds for the one and two properties but fails for the oneX and twoX properties, giving you exactly the errors you wanted.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Thanks! I'd like to extend my question to include the goal of having the function return whatever type was finally inferred. Can I update this question or should it be a new question? – Jason Kuhrt Apr 06 '23 at 02:10
  • I gave it a small update at the bottom with a playground link of a failed attempt to return he inferred type. – Jason Kuhrt Apr 06 '23 at 02:17
  • I don't know how to deal with questions whose scopes change after I've answered them. I don't quite understand what your issue is (something is presumably unacceptable about returning `typeof xs` but I don't get what it is, or what "inferred type" you're talking about) and this isn't an appropriate place to discuss it, unfortunately. I'd suggest you open a new question about it. – jcalz Apr 06 '23 at 02:21
  • I posted a focused and hopefully clear question here https://stackoverflow.com/questions/75945737/how-can-an-inferred-parameter-type-be-referenced-in-return-type-position. – Jason Kuhrt Apr 06 '23 at 04:10