#Trying to teach `infer`. Realizing I don't know it myself. Please help with this simple example

290 messages Β· Page 1 of 1 (latest)

cyan laurel
#

Title kinda says it all.
I get the rather unhelpful error on my return as

Type '{ __typename: "Crumbs"; kind: string; }' is not assignable to type 'Food extends { kind: infer U; } ? { __typename: "Crumbs"; kind: U; } : never'.ts(2322)

Can anybody help explain? Or can anyone think of a better example to teach infer with? I was working up to this example and went from function signatures --> generics --> constraining generics with extends --> and finally I needed an example of using infer in the return. So if there's any way I could salvage this example, that'd be much appreciated

#
const feedCookieMonster = <Food extends { kind: string }>(
  food: Food
): (typeof food) extends { kind: (infer U) }
  ? { __typename: 'Crumbs'; kind: U }
  : never => {
  console.log('munch munch');
  return { __typename: 'Crumbs', kind: food.kind };
};

const cookie = { kind: 'peanut butter' } as const;
const theCrumbs = feedCookieMonster(cookie);
//     ^?
#

(yes I will admit I had to ask for help lol)

gusty nest
cyan laurel
#

cool project but I'm not trying to hack or work around TypeScript. just want the canonical way to use infer to grab a property off a generic and make the return vary based on that

gusty nest
#

you just want to know how it works? assuming you read the docs?

cyan laurel
#

I've read the docs. They don't have an entry on infer I could find except in the release notes for when it came out. I thought I understood how it works. Really what I'm asking is why

Type '{ __typename: "Crumbs"; kind: string; }' is not assignable to type 'Food extends { kind: infer U; } ? { __typename: "Crumbs"; kind: U; } : never'

#

and what exactly I'm doing wrong. If I don't use javascript and just declare a type signature or something I can get it working just find. I guess my question is how are you actually supposed to use this in practice?

half wing
#

is there a reason you don't let inference do the work

#

(it will probably infer { __typename: 'Crumbs'; kind: Food["kind"]; })

cyan laurel
#

@half wing well the point is to come up with a good example of using infer. But also we have that annoying ESLint rule to explicitly add return types to our functions

half wing
#

well

cyan laurel
#

it's inference does

{
    __typename: string;
    kind: unknown;
} | null
half wing
#

conditional return types are a pretty bad example for using infer honestly

cyan laurel
#

which is not quite as useful

half wing
half wing
half wing
#

but imo conditional types are best used for generating types based on other types

cyan laurel
#

right that's fair but I'm not trying to solve this problem so much as trying to come up with a good example of infer that also fits a story I constructed and worked up to lol. Any suggestions for a better example of infer?

half wing
half wing
cyan laurel
#

typechecking simply doesn't work for conditional return types

Oh?

half wing
#

e.g. mapping tuple types

half wing
#

when using it it works fine, of course

#

but it's a pretty huge loss not being sure your function even does the right thing imo

cyan laurel
#

Yeah I mean I also have "practical" examples like

ElementFromArray<T> = T extends (infer U)[] ? U : T

And ReturnType and PromiseResult and other such examples. But I was really hoping to find a good use-case specifically to a function since that's been what my story has been focused on so far

tulip snowBOT
half wing
#

!ts

tulip snowBOT
#
function foo<T>(x: T): T extends number ? string : number {
    return 1
//  ^^^^^^^^
// Type 'number' is not assignable to type 'T extends number ? string : number'.
}```
half wing
cyan laurel
#

lol yeah but that seems hackier than infer

half wing
#

._.

cyan laurel
#

I've seen T[0] in the codebase before

#

for a similar situation

half wing
#

T[number] is the conventional way to get array elements

half wing
#

!hb indexed

tulip snowBOT
half wing
#

Another example of indexing with an arbitrary type is using number to get the type of an array’s elements. We can combine this with typeof to conveniently capture the element type of an array literal:

cyan laurel
#

Fine ElementFromArray isn't the best example but ReturnType and all that are great infer examples. Either way my main point was trying to find a good use-case for infer in the contexts of a function

half wing
#

i'm not sure there is a good use-case

#

since virtually all, if not literally all, will require as any or other sorts of jank like that

cyan laurel
#

well. That's something to think about

#

Hmmm. This might be the resolution to my ticket haha

#

and an important note in the lesson

half wing
#

like, maybe you could make a function like: function foo<T /* explicitly specify this */>(value: Model<T>) where Model is a conditional type that creates a schema given a type or something

#

or the other way around:

function foo<T extends Model<unknown>>(value: T)
#

but even then that's pretty niche

#

and almost certainly not something that should be covered in a tutorial that covers the basics

gusty nest
cyan laurel
#
/**
 * If given an array, return the array.
 * If given a falsey value, return an empty array.
 * If given a non-array, return an array with the value as its only element.
 */
export const arraySafely = <T>(
  array: T
): SafeArray<T> =>
  [array || []].flat() as SafeArray<T>

type SafeArray<T> = (NonNullable<T> extends readonly (infer U)[] ? U : NonNullable<T>)[];
#

this is as bad as doing as any I guess

#

but what do you think of this use-case

half wing
#

ummm

cyan laurel
#

lemme rewrite real quick

half wing
#

it's... a tiny bit better than as any i guess?

#

tangent: the return type of .flat() already uses a conditional type with infer

cyan laurel
#

does it? I'm pretty sure I've been frustrated many times by .flat losing my typing

#

source code says

    flat<A, D extends number = 1>(
        this: A,
        depth?: D
    ): FlatArray<A, D>[]
#

oh I see

type FlatArray<Arr, Depth extends number> = {
    "done": Arr,
    "recur": Arr extends ReadonlyArray<infer InnerArr>
        ? FlatArray<InnerArr, [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20][Depth]>
        : Arr
}[Depth extends -1 ? "done" : "recur"];
#

wow that's bizarre

half wing
#

yeah indeed

#

it's one of the most unusual types in the stdlib i'm pretty sure

cyan laurel
#

I guess using strings like 'done' and 'recur' like that makes the code slightly more self-documenting

#

?

half wing
#

🀷 lol i guess

#

pretty wild though,i don't think i've seen that pattern anywhere else

half wing
#

but also the typescript way to do this is to just...

#

... pass an array in, in the first place

cyan laurel
#

I made it because someone else had an arraySafety that returned any and caused a bunch of issues in our code base

Figured if they were gonna use it, this is nominally better.

Plus it is kinda conventient when working with data that could either be an array of the thing you want or null or undefined

half wing
#

well

cyan laurel
#

but yeah point taken about other falsey values caught in the crossfire like ""

#

that just wasn't ever an issue where this was used

tulip snowBOT
half wing
#

!ts

tulip snowBOT
#
export function arraySafely<T extends readonly unknown[]>(array: T): T;
export function arraySafely<T>(item: T): T[];
export function arraySafely<T>(array: T): T | T[] {
  return Array.isArray(array) ? array : [array];
}
const x = arraySafely(1);
//    ^? - const x: number[]
const y = arraySafely([1, 2, 3] as const);
//    ^? - const y: readonly [1, 2, 3]```
half wing
#

no infer 😌

#

now overloads aren't completely typesafe either

#

but i think this one looks much easier to read, at least

half wing
cyan laurel
#

there's 0 instances of overloads being used anywhere in our code haha

#

maybe I should start suggesting it

#

Also sorry @gusty nest didn't mean to ignore you. I understand structural subtyping (or at least well enough to get why TS has its defaults) but I don't think I follow how that's relevant.

Maybe it's relevant to answering my current iteration of my main question which is:

Why doesn't typescript support conditional type checking on return values?

gusty nest
#

I wasn't sure if you did or not, my mistake

#

sorry about that

#

I think it's because you can't guarantee immutability and ensure it will remain a proper subset

half wing
gusty nest
#

but im not sure

half wing
#

you could use control flow narrowing to check some of the conditional types

cyan laurel
#

right

half wing
#

but there's no way at runtime to distinguish between function types, for example

half wing
half wing
gusty nest
#

that's why I'm studying the typescript examples I linked above. I cannot find a better resource for using conditionals and inference with comments on how it works

cyan laurel
#

which is what I expected TS would do. But anyways, when you explicitly type the return of a TS function TS is not worried about the internals of that function when used elsewhere, right? It just kinda takes your word for it? So all it really has to do is ensure that that type you specified is possible?

half wing
gusty nest
#

even official docs etc

gusty nest
half wing
#

well

#

the internals never matter

#

even without explicit typing, it puts an inferred type on the function

cyan laurel
#

Hmm I see. I suppose I'm confusing the matter anyways. The main point of discussion/original post is about why TypeScript's inference (which is obviously capable of type narrowing) wasn't able to check if that narrowed type can be assigned to a conditional type

half wing
#

and uses the inferred return type when checking everywhere else

gusty nest
#

my opinion is that the compiler writers just arent as good as those working on haskell etc

#

but dont tell them down there

cyan laurel
#

lol

gusty nest
#

πŸ˜‰

half wing
cyan laurel
#

I haven't used langs with advanced typescript before tbh. Would Haskell be able to handle something like this?

#

are conditional types common?

#

(I'm a junior)

half wing
cyan laurel
#

right I got the impression that TS is rather unique

gusty nest
#

haskell has things in place of it

half wing
#

virtually all languages other than typescript don't have such a messy type system

half wing
gusty nest
#

they have kinds, which are functions of types

#

and GADTs

gusty nest
half wing
#

this is what makes. most languages tidier

cyan laurel
#

right I guess the only fair thing to compare it too is a type system built on top of a dynamically typed language

half wing
#

ok not really

#

its the lack of structural typing

#

if you say a thing is a certain type

#

it is that type

#

its not "oh it has the same shape"

#

so if you add a method (typeclass impl) onto that type

#

it works for all instances of that type

#

haskell also lacks unions that arent discriminated

half wing
gusty nest
#

what is the usecase for undiscriminated unions

half wing
#

none

gusty nest
#

ah, yeah

#

lol

half wing
#

its just something that typescript must support

cyan laurel
#

tbh the undiscriminated unions are pretty cool imo

#

Would you be able to do MyArray[number] without them?

gusty nest
#

I assure you

#

they are not cool

#

lol

cyan laurel
#

Or MyObject['something' | 'somethingElse']? I thought that was only possible because of TS's undiscriminated unions

#

Or I guess what I meant is

(MyObject1 | MyObject2)['field']

#

@half wing would it be safe to say that conditional return types with infer should generally be avoided?

#

or @gusty nest if you have thoughts

gusty nest
#
Tree a = Leaf | Branch a (Tree a) (Tree a)
find x Leaf = Nothing
find x Branch a left right | x == a = Just a
find x Branch a Leaf right = find x right
find x Branch a left Leaf  = find x left
find x Branch a left right = case find x left of 
                               Just a -> Just a
                               Nothing -> find x right
#

there are more elegant ways to show that example but you get the idea, discriminated unions make things a lot easier

gusty nest
cyan laurel
#

!resolved

#

Idk haskell lol

#

Thanks so much for the discussion and advice/help

gusty nest
#

it just says a tree is a leaf or a branch with a value and a left and right subtree

#

so if you want to find something you just look at where you are and decide where to go next

#

but yeah!!

#

take care

cyan laurel
#

ahh I see

#

But TypeScript's type narrowing kinda serves the same role, right?

gusty nest
#

it sure tries

tulip snowBOT
gusty nest
#

im sure it is

#

its 2 am

cyan laurel
#

I admit that looks pretty elegant. I should finish my Elm project... Just need to find a time when I'm not on a "this needs to get done ASAP or the company fails" project smh

#

oof, tomorrow's Monday. Go to bed! Night!

half wing
#

also find x Branch a left right should be find x (Branch a left right)

cyan laurel
#

yeah maybe I'm not fully following what y'all mean about undiscriminated unions, but I use patterns like

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };

All the time. What are the situations when TS is undiscriminated

half wing
#

when there's no kind

tulip snowBOT
half wing
#

!ts

tulip snowBOT
#
type Maybe<a> = ['Just', a] | ['Nothing']
const or_ = <a,>(a: () => Maybe<a>, b: () => Maybe<a>): Maybe<a> => {
  const a_ = a()
  return a_[0] === 'Nothing' ? b() : a_
}
type Tree<a> = ['Leaf'] | ['Branch', a, Tree<a>, Tree<a>]
const find = <a,>(x: a, tree: Tree<a>): Maybe<a> => {
  switch (tree[0]) {
    case 'Leaf': { return ['Nothing'] }
    case 'Branch': {
      const [, a, left, right] = tree
      if (x === a) { return ['Just', a] }
      return or_(() => find(x, left), () => find(x, right))
    }
  }
}
console.log(find(1, ['Branch', 0, ['Branch', 1, ['Leaf'], ['Leaf']], ['Leaf']]))
//          ^? - const find: <number>(x: number, tree: Tree<number>) => Maybe<number>
console.log(find(2, ['Branch', 0, ['Branch', 1, ['Leaf'], ['Leaf']], ['Leaf']]))
//          ^? - const find: <number>(x: number, tree: Tree<number>) => Maybe<number>```
half wing
#

@gusty nest discriminated unions work perfectly fine in ts

#

like, yes, the syntax is quite clunky

gusty nest
#

i know they work

half wing
#

but at least it's fully types safe

gusty nest
#

but its awful

half wing
#

🀷

gusty nest
#

thats how we write our code

half wing
#

more than readable enough to me

#

it's way more readable with helper functions

gusty nest
#

it just makes my smol brain swell

cyan laurel
#

what's with this a convention. Is this common in functional langs?

gusty nest
#

its a thing

#

you dont know what it is

cyan laurel
#

i hate it

gusty nest
#

it doesnt matter

#

that came out wrong

#

i mean "a" is a thing which you dont know or care what it is

#

generally

#

just something you've got along for the ride

cyan laurel
#

haha i see i see. yeah makes sense just seems like a terrible variable name

gusty nest
#

what would you call it

#

Thing?

cyan laurel
#

but then again <T>

#

sure. I think Thing makes more sense. Also, at least capitalize it per the typical convention with generics

#

anyways, just nitpicking

gusty nest
#

the convention is to uncapitalize it

#

idk why typescript doesnt

tulip snowBOT
half wing
#

!ts

tulip snowBOT
#
type Maybe<a> = ['Just', a] | ['Nothing']
const Maybe_match = <a, b>([tag, a]: Maybe<a>, onJust: (a: a) => b, onNothing: () => b): b => {
  switch (tag) {
    case 'Just': { return onJust(a) }
    case 'Nothing': { return onNothing() }
  }
}
const or_ = <a,>(a: () => Maybe<a>, b: () => Maybe<a>): Maybe<a> => Maybe_match(a(), a => ['Just', a], b)
type Tree<a> = ['Leaf'] | ['Branch', a, Tree<a>, Tree<a>]
const Tree_match = <a, b>([tag, a, left, right]: Tree<a>, onLeaf: () => b, onBranch: (a: a, left: Tree<a>, right: Tree<a>) => b): b => {
  switch (tag) {
    case 'Leaf': { return onLeaf() }
    case 'Branch': { return onBranch(a, left, right) }
  }
}
const find = <a,>(x: a, tree: Tree<a>): Maybe<a> => Tree_match(
  tree,
  () => ['Nothing'],
  (a, left, right) =>
    x === a ? ['Just', a] :
    or_(() => find(x, left), () => find(x, right))
)
console.log(find(1, ['Branch', 0, ['Branch', 1, ['Leaf'], ['Leaf']], ['Leaf']]))
//          ^? - const find: <number>(x: number, tree: Tree<number>) => Maybe<number>
console.log(find(2, ['Branch', 0, ['Branch', 1, ['Leaf'], ['Leaf']], ['Leaf']]))
//          ^? - const find: <number>(x: number, tree: Tree<number>) => Maybe<number>```
cyan laurel
#

this is Kotlin, but this was my only exposure to generics before TS

half wing
#

see: c#, java, rust, scala

#

and almost every other language

#

worth noting that rust is heavily fp.

cyan laurel
#

that was my impression as well

gusty nest
#

the languages i've seen generally all use lowercase

#

i guess its split

half wing
#

ocaml has lowercase generic type parameters

#

but that's because all of ocaml's types are lowercase...

cyan laurel
#

the thing that sucks across all languages is that generics are so often a single character long. Like why...

half wing
#

well, in general it's because there's no explanation needed

gusty nest
#

f#, elm, haskell, ML

half wing
gusty nest
#

but it doesnt matter

half wing
gusty nest
#

haha lets not nitpick

half wing
#

so it'd be weirder if they didn't use lowercase

cyan laurel
#

K but sometimes you could avoid having to leave a comment by simply writing ArrayElement instead of T

gusty nest
#

every language is a derivative

half wing
#

the way i see it:

  • languages derived from c++ ("OOP") (at least, syntactically similar) use uppercase
  • languages derived from haskell and ml ("FP") use lowercase
gusty nest
#

yeah that makes sense to me

half wing
#

not like there's an advantage to either tbh, i'd wager it's just what happened to use

half wing
cyan laurel
#

sometimes you have complex types (especially in TS given the only conditional logic has to be done with ternaries) and it's easy to get lost. Named generics can go a long way there

half wing
#

well

#

firstly, yes

gusty nest
#

eh who cares

cyan laurel
#

me I guess

half wing
#

with multiple arguments, nobody uses single-letter names

#

other than the fp people

#

πŸ˜”

cyan laurel
#

we dont talk about the fp people

half wing
#

ok like genuinely

#

fp-ts type parameter naming

#

like

#

why

gusty nest
#

i hate it even worse

#

i dont get it

#

its like they want people to hate FP

half wing
#

this is exactly why all the big libraries that have multiple type parameters, name the parameters

#

well, the real reason is so that you actually know which parameter does what...

#

but also i think conditional logic is just... avoided in general

cyan laurel
#

Java

    E βˆ’ Element, and is mainly used by Java Collections framework.
    K βˆ’ Key, and is mainly used to represent parameter type of key of a map.
    V βˆ’ Value, and is mainly used to represent parameter type of value of a map.
    N βˆ’ Number, and is mainly used to represent numbers.
    T βˆ’ Type, and is mainly used to represent first generic type parameter.
    S βˆ’ Type, and is mainly used to represent second generic type parameter.
    U βˆ’ Type, and is mainly used to represent third generic type parameter.
    V βˆ’ Type, and is mainly used to represent fourth generic type parameter.
half wing
#

why the HECK is S the second type parameter

#

D:

gusty nest
#

cause its java

half wing
#

when i use type parameters that literally have no better name

#

i do T, U, V, W, X, Y, Z, A

cyan laurel
#

T, U, V

half wing
#

like a normal person

cyan laurel
#

yeah

#

nomral af

half wing
#

XD

cyan laurel
#

Java also counts 2, 1, 3, 4

half wing
gusty nest
#

I use unicode emojis

half wing
#

T extends U & V & W & X & Y & Z & A & B

half wing
#

respectable

cyan laurel
#

tbh at least they're unique and easy to follow

#

LGTM

#

y'all use i, j, k as your indexes when they get nested, right? What comes after k (doG forbid)?

gusty nest
#

I haven't used an index in years tbh

cyan laurel
#

hmm

half wing
cyan laurel
#

that's kinda wild to me. I could see why relying on indexes can sometimes be code smell, but years??

gusty nest
#

polynomial time baby

cyan laurel
#
tablesToRender
rowGroupsToRender
rowsToRender
columns

idk?

#

oh yeah also to avoid React key warnings

// eslint-ignore that-one-rule-about-no-indexes-as-keys
key={i}
#

tbh I feel like that's the most common use of indexes in the app lol

gusty nest
#

true, you got me. I do use list.map((item, i) => <li key={i} />

cyan laurel
#

that warning is safe to ignore if your data has no chance of every getting reordered, right?

#

wow this thread has gotten so off topic lol sorry

gusty nest
#

or added or removed yeah

cyan laurel
#

alright. Thanks again for the chat y'all. I'ma sign off for the night

gusty nest
#

good night

left mesa
#

using infer inside tuples and template string types are quite useful