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 : falseTypescript 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> // falseConditionals 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> // falseThe 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> // falseThe 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 : falseIt 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 errorThe 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'}) // unknownTypescript 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;
}): unknownYou 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` typeThe 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) // stringBut 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'}) // stringThis 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}