#Unexpected `unknown` when using indexed access on a generic

21 messages · Page 1 of 1 (latest)

plush mesa
#

I'm trying to figure out why ex2 stands out the way it does, and if there's a way to fix it. In this case, the 'fix' would be it resolving to undefined or never.
It's probably a side effect from how {} behaves, since it apparently equals NonNullable<unknown> rather than Record<string, never>.

lavish galleonBOT
#
lecarbonator#0

Preview:```ts
type Test = {
foo?: string
}

type Foo<T extends Test> = {
test?: T
}

function foo<T extends Test = Record<string, never>>(
opts: Foo<T>
): T["foo"] {
return opts.test?.foo
}

const ex1 = foo({})
// ^?
const ex2 = foo({test: {}})
// ^?
const ex3 = foo({test: {foo: undefined}})
...```

severe vector
#

since it apparently equals NonNullable<unknown> rather than Record<string, never>
this is expected, though it might be more direct to say NonNullable<unknown> is {} - it kinda acts like {} | null | undefined, kinda like how boolean acts like true | false

#

there's no real "empty object" type - {} is a type that doesn't require anything other than being indexable, so any non-nullish value

#

Record<string, never> is almost never the type you want

plush mesa
#

in this case, Record<string, undefined> would describe the type better yeah

severe vector
#

also, setting defaults on function generics kinda breaks inference

severe vector
plush mesa
#

Well, in this case, it would accurately describe what foo() actually returns, no?

severe vector
#

you generally don't want to describe stuff exactly, just their constraints

#

the behaviour here specific might be because of the optional?

plush mesa
#

Okay, so apart from the eager constraint with undefined or string, the unknown is way wider than the constraint could ever be

lavish galleonBOT
#
type Test = {
    foo?: string | undefined
}

type X = {} extends Test ? true : false
//   ^? - type X = true
severe vector
#

there's some soundness gaps regarding optionals

#

if you hover over foo in the call, you can see that T is being inferred to {}

#

which, unfortunately, does extend Test

plush mesa
#

Hm, looks like that's not the only part. For example

lavish galleonBOT
#
lecarbonator#0

Preview:```ts
type Test = {
foo?: string
bar: number
}

type Foo<T extends Test> = {
test?: T
}

function foo<T extends Test>(opts: Foo<T>): T["foo"] {
return opts.test?.foo
}

const ex1 = foo({})
// ^?
const ex2 = foo({test: {bar: 0}})
// ^?
const ex3 = foo({test: {foo: undefined, bar: 0}})
...```

plush mesa
#

even here, T['foo'] evaluates to unknown

severe vector
#

well, same thing, really?

lavish galleonBOT
#
chris.sophos#0

Preview:```ts
type Test = {
foo?: string
bar: number
}

type Foo<T extends Test> = {
test?: T
}

function foo<T extends Test>(
opts: Foo<T>
): T extends {foo: infer V} ? V : undefined {
return opts.test?.foo
}

const ex1 = foo({})
// ^?
const ex2 = foo({test: {bar: 0}})
...```