Advanced TypeScript: Generic Constraints and Type Compatibility
Welcome to the Generic Constraints and Type Compatibility lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
We've seen type compatibility at many points in this course. Now we'll look at it more closely, paying special attention to how it interacts with generic constraints.
As a simple example, we can assign a literal string type like
'name'to astring. That works because the'name'type is a kind of string. The strings below live inside of an object, but the same compatibility rules apply.>
const aName: {key: 'name'} = {key: 'name'};const aString: {key: string} = aName;aString;Result:
{key: 'name'}We can also assign any union of literal string types to a
string.>
const nameOrAge: {key: 'name' | 'age'} = {key: 'age'};const aString: {key: string} = nameOrAge;aString;Result:
{key: 'age'}We can assign a literal string type to a union that contains that string.
>
const aName: {key: 'name'} = {key: 'name'};const nameOrAge: {key: 'name' | 'age'} = aName;nameOrAge;Result:
{key: 'name'}However, we can't assign a literal string type to a union that doesn't include that literal string type.
>
const aName: {key: 'name'} = {key: 'name'};const emailOrAge: {key: 'email' | 'age'} = aName;emailOrAge;Result:
type error: Type '{ key: "name"; }' is not assignable to type '{ key: "email" | "age"; }'. Types of property 'key' are incompatible. Type '"name"' is not assignable to type '"email" | "age"'.We also can't assign a union to another incompatible union, even if they overlap partially. In the example below, we try to use a
'name' | 'age'as an'email' | 'age'. If the actual runtime value is'age', that's fine. But if it's'name', then it's not fine! When type checking at compile time, TypeScript can't predict which value we'll use. Although the value ofnameOrAgeis{key: 'age'}, its type is still{key: 'name' | 'age'}>
const nameOrAge: {key: 'name' | 'age'} = {key: 'age'};const emailOrAge: {key: 'email' | 'age'} = nameOrAge;emailOrAge;Result:
type error: Type '{ key: "name" | "age"; }' is not assignable to type '{ key: "age" | "email"; }'. Types of property 'key' are incompatible. Type '"name" | "age"' is not assignable to type '"age" | "email"'. Type '"name"' is not assignable to type '"age" | "email"'.Now we'll explore how these type compatibility rules interact with generic constraints like
<Row extends {age: number}>, which we saw in an earlier lesson.In the next example, we define a
SpecificKey<T>type. It gives us object types whosekeyproperty must beT. For example,SpecificKey<'name'>gives us the type{key: 'name'}.>
type SpecificKey<T extends string> = {key: T};const nameKey: SpecificKey<'name'> = {key: 'name'};nameKey.key;Result:
'name'
>
type SpecificKey<T extends string> = {key: T};const ageKey: SpecificKey<'name'> = {key: 'age'};ageKey.key;Result:
type error: Type '"age"' is not assignable to type '"name"'.
There's a subtle but important point shown in those examples. We declared our type parameter as
<T extends string>, but the actual type ofTwas'name'.The generic constraint
<T extends string>doesn't mean thatTwill be exactlystring. Instead,Tmust be some type that is compatible withstring. That type could be the literal string'name', or a union of literal strings like'name' | 'age', or even the fullstringtype. All of those are compatible withstring, so we can use any of them asT.The next examples show that our
SpecificKey<T>type works with unions. It gives us object types like{key: 'name' | 'age'}.>
type SpecificKey<T extends string> = {key: T};const nameOrAge: SpecificKey<'name' | 'age'> = {key: 'age'};nameOrAge.key;Result:
'age'
>
type SpecificKey<T extends string> = {key: T};const nameOrAge: SpecificKey<'name' | 'age'> = {key: 'email'};nameOrAge.key;Result:
type error: Type '"email"' is not assignable to type '"name" | "age"'.
Like before, we're not allowed to mix unions up, even if they overlap partially.
>
type SpecificKey<T extends string> = {key: T};const nameOrAge: SpecificKey<'name' | 'age'> = {key: 'age'};const emailOrAge: SpecificKey<'email' | 'age'> = nameOrAge;emailOrAge;Result:
type error: Type 'SpecificKey<"name" | "age">' is not assignable to type 'SpecificKey<"age" | "email">'. Type '"name" | "age"' is not assignable to type '"age" | "email"'. Type '"name"' is not assignable to type '"age" | "email"'.We'll see some more realistic examples of these ideas in future lessons. For now, it's important to recognize that the usual type compatibility rules still apply in generic constraints. A constraint like
<T extends string>allows any type that's compatible withstring.