2

I have encountered strange behavior. TypeScript version: 4.9.4 Playground

interface TestInterface {
    A: string
}

type Test = TestInterface extends Record<infer K, any> ? K : never;
let t1: Test; // A


type Test2 = TestInterface extends Record<any, infer V> ? 1 : never;
let t2: Test2; // never


type Test3 = TestInterface extends Record<infer K, infer V> ? V : never;
let t3: Test3; // string

I have interface TestInterface.

  • When I try to infer only keys, it works.
  • When I try to infer values, will never.
  • When I try to infer keys and values, it works..

Why?

Evgeny Naumov
  • 347
  • 1
  • 4
  • 15
  • 1
    If you make `TestInterface` a type (use `type` instead of `interface`) - it would work. I think it has something with indexing. See [this](https://stackoverflow.com/questions/37233735/interfaces-vs-types-in-typescript#answer-64971386) question. Also, please see [this](https://stackoverflow.com/questions/58251698/does-this-typescript-example-violate-the-liskov-subtitution-principle) answer and [this](https://github.com/microsoft/TypeScript/issues/15300#issuecomment-332366024) comment. It appears that it is safer to infer type index signature than interface – captain-yossarian from Ukraine Jan 17 '23 at 12:28
  • 1
    `Record` is equivalent to the index signature type `{[k: string]: infer V}`, and interfaces do not get implicit index signatures, so there's no match. This is one of the few places where you can't just use `any` to mean "I don't care". If you need to write that without doing `infer K` then you should use `keyof TestInterface` instead of `any`. Does that fully address your question? If so I could write up an answer explaining with links to sources. If not, what am I missing? – jcalz Jan 17 '23 at 15:22

1 Answers1

0

Record has 2 behaviours and it gets people confused all the time, but interfaces and index signatures also get people confused.

type Record<K extends string | number | symbol, T> = { [P in K]: T; }

When K is a union of subtypes of string, number or symbol, like 'A' | 'B', it results in an object literal type, like the following:

{ A: string, B: string }

An object literal type says:

I know about the fields listed here, but there can be any number of supernumerary fields and they can be of any type

When K is a union of exactly string, number or symbol, like string | number, it results in a type signature, like the following

{
   [key: string]: string
   [key: number]: string
}

A type signature basically says:

every possible field matching this type of key will be of this very type

And that's very different.

Record is also a bit weird in that Record<any, unknown> evaluates to {[k: string]: unknown} contrary Record<PropertyKey, unknown> or { [P in any]: unknown } but that's not what caused your surprise.

Bottom line is: when you extend either an object literal type or an index signature, it's not at all the same kind of check.


The second confusing thing is: interfaces are not finite, because of declaration merging.

interface TestInterface {
    A: string
}

interface TestInterface {
    B: number
}

// same as
interface TestInterface {
    A: string
    B: number
}

If you can merge it at will, and even use module augmentation, it's hard to say that an interface extends an index signature at any given time — at least that's my mental model and it works quite well.

The only exception is if you constrain your interface to a specific index signature explicitly:

interface TestInterface {
    [k: string]: string
    A: "foo"
}

interface TestInterface {
    B: number
//  ~~~~~~~~~ nope!
}

Then you are in a position to make promises: if you extends Record<any, infer V>, V will be exactly string (even though A is "foo")


Now, as mentioned in the comments, when you write keyof TestInterface you get the interface's keys. Using them in Record will return an object literal type, and you will be good.

interface TestInterface {
    A: string
}
type S = TestInterface extends Record<keyof TestInterface, infer V> ? V : never; // string

Just take note that this is only true if the interface does not have an explicit index signature: if it has one, it won't return known keys, such as "A" but something like string | number (you always get number when the key is string) which will return an index signature when fed to Record.

geoffrey
  • 2,080
  • 9
  • 13
  • Thanks for the detailed explanation, but I don't understand your phrase **I know about the fields listed here, but there can be any number of supernumerary fields and they can be of any type**. In this type `{ A: string, B: string }` possible fields only `'A' | 'B'` and we know about field types. Why there can be any number of supernumerary fields if I listed only `'A' | 'B'`? – Evgeny Naumov Jan 19 '23 at 06:31
  • Excess property checking fires in a couple of places where TS assumes you probably don't intend to have supernumerary properties: when you use an object literal expression as the initializer of an annotated variable declaration, and when you use an object literal expression as an argument in a call expression; But at the type level, it works exactly as I have explained and you should always assume it is so as an implementer anyway because people can pass references to your functions. https://tsplay.dev/NrDO1W – geoffrey Jan 19 '23 at 12:17