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}