Skip to content

Instantly share code, notes, and snippets.

@NicholasBoll
Created April 20, 2022 16:20
Show Gist options
  • Save NicholasBoll/689fb5999d23895da17070f699773517 to your computer and use it in GitHub Desktop.
Save NicholasBoll/689fb5999d23895da17070f699773517 to your computer and use it in GitHub Desktop.
Typescript Infer keyword

Typescript has conditionals like JavaScript does, but they are different.

In JavaScript, conditionals can compare values:

if (a > 1) {
  return true
} else {
  return false
}

JavaScript has other conditional structures like switch and ternaries. The above could be written like:

return a > 1 ? true : false

Typescript has conditionals, but can only compare types. Typescript is a structural type system as opposed to a nominal type system. That means Typescript compares the shape of a type and not the inheritance chain like Java does.

So a conditional in Typescript looks like this:

type Foo<T> = T extends string ? true : false

Foo<'foo'> // true
Foo<string> // true
Foo<1> // false
Foo<number> // false

Conditionals can also be used by non-primatives:

type Foo<T> = T extends { foo: string } ? true : false

Foo<{foo: string}> // true
Foo<{foo: 'bar'}> // true
Foo<{foo1: string}> // false
Foo<1> // false

The infer keyword extends the functionality of conditionals by allowing a position in the shape to be inferred while testing the shape:

type Foo<T> = T extends { foo: infer S } ? S : false

type Foo<{foo: 'bar'}> // 'bar'
type Foo<{foo: string}> // string
type Foo<number> // false
type Foo<string> // false

The infer keyword cannot be used for constraints though:

// 'infer' declarations are only permitted in the 'extends' clause of a conditional type.ts (1338)
type Foo<T extends { foo: infer S}> = T extends { foo: infer S } ? S : false

It is possible to make sure the false branch of the conditional is never hit by combining both a constraint with an infer conditional:

type Foo<T extends {foo: any}> = T extends { foo: infer S } ? S : false

Foo<{foo: string}> // string
Foo<{foo: 'bar'}> // 'bar'
Foo<number> // type error
Foo<string> // type error

The infer keyword is useful in deffering type inference of a type as well as decrease the burden of a primary generic. Let's look at the following example:

function getFoo<S, T extends {foo: S}>(input: T): S {
  return input.foo
}

foo({foo: 'bar'}) // unknown

Typescript tries to infer S and T at the same time, but doesn't have enough information. The S will be assigned unknown while the T will be assigned {foo: 'bar'}. If you inspect the call signature of the getFoo function in this example, it will look like this:

function getFoo<unknown, {
    foo: string;
}>(input: {
    foo: string;
}): unknown

You can see the generic S is used in 2 places: the return type of getFoo as well as the constraint of the generic T. Typescript doesn't know the return type from the assignment. You could cast the return type to give the call signature enough information:

const bar = getFoo({foo: 'bar'}) as 'bar' // 'bar'

You can see how the type of 'bar' is now listed in code twice. Typescript will at least prevent errors of incompatible casting:

const bar = getFoo({foo: 'bar1'}) as 'bar'
//          ~~~~~~
            // Type '"bar1"' is not assignable to type '"bar"'. ts(2322)

But the fact remains that 'bar' appears twice in code and this is not what we want. The infer keyword instead delays inference of the generic S until later, allowing Typescript to have enough information about the input type and not relying inference of the return type as the same time as inference of input:

function foo<T extends {foo: any}>(
  input: T
): T extends {foo: infer S} ? S : never {
  return input.foo;
}

getFoo({foo: 'bar'}) // string
getFoo('bar') // Type error because of the constraint. Without the constraint, the return type would be `never`
getFoo({foo: 'bar' as const}) // 'bar' - the `as const` instructs Typescript to narrow the type of `'bar'`` to a literal instead of the wider `string` type

The infer keyword helps to properly type JavaScript. If you write Typescript with explicit types, you do not need the infer keyword. infer can still be very useful with Typescript if you don't care for heavily explicitly-typed code.

For example, if we always fill generics of functions, the following would work without infer:

interface Foo {
  foo: string
}

function getFoo<S, T extends {foo: S}>(input: T): S {
  return input.foo
}

const input: Foo = {foo: 'bar'}
const bar = getFoo<string, Foo>(input) // string

But the following example uses the getFoo function that uses infer and is much cleaner without explicitly typing variables and generics:

const bar = getFoo({foo: 'bar'}) // string

This style is also convenient if you don't need to care about the explicit type:

// In React:
type ComponentProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> =
  T extends JSXElementConstructor<infer P>
    ? P
    : T extends keyof JSX.IntrinsicElements
      ? JSX.IntrinsicElements[T]
      : {};

const MyComponent = (props: {foo: string}) => <div />

type Props = React.ComponentProps<typeof MyComponent> // {foo: string}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment