Was this page helpful?

This page has been deprecated

This handbook page has been replaced,go to the new page

Advanced Types

This page lists some of the more advanced ways in which you can model types, it works in tandem with theUtility Types doc which includes types which are included in TypeScript and available globally.

Type Guards and Differentiating Types

Union types are useful for modeling situations when values can overlap in the types they can take on.What happens when we need to know specifically whether we have aFish?A common idiom in JavaScript to differentiate between two possible values is to check for the presence of a member.As we mentioned, you can only access members that are guaranteed to be in all the constituents of a union type.

ts
letpet =getSmallPet();
 
// You can use the 'in' operator to check
if ("swim"inpet) {
pet.swim();
}
// However, you cannot use property access
if (pet.fly) {
Property 'fly' does not exist on type 'Fish | Bird'. Property 'fly' does not exist on type 'Fish'.2339Property 'fly' does not exist on type 'Fish | Bird'. Property 'fly' does not exist on type 'Fish'.
pet.fly();
Property 'fly' does not exist on type 'Fish | Bird'. Property 'fly' does not exist on type 'Fish'.2339Property 'fly' does not exist on type 'Fish | Bird'. Property 'fly' does not exist on type 'Fish'.
}
Try

To get the same code working via property accessors, we’ll need to use a type assertion:

ts
letpet =getSmallPet();
letfishPet =petasFish;
letbirdPet =petasBird;
 
if (fishPet.swim) {
fishPet.swim();
}elseif (birdPet.fly) {
birdPet.fly();
}
Try

This isn’t the sort of code you would want in your codebase however.

User-Defined Type Guards

It would be much better if once we performed the check, we could know the type ofpet within each branch.

It just so happens that TypeScript has something called atype guard.A type guard is some expression that performs a runtime check that guarantees the type in some scope.

Using type predicates

To define a type guard, we simply need to define a function whose return type is atype predicate:

ts
functionisFish(pet:Fish |Bird):petisFish {
return (petasFish).swim !==undefined;
}
Try

pet is Fish is our type predicate in this example.A predicate takes the formparameterName is Type, whereparameterName must be the name of a parameter from the current function signature.

Any timeisFish is called with some variable, TypeScript willnarrow that variable to that specific type if the original type is compatible.

ts
// Both calls to 'swim' and 'fly' are now okay.
letpet =getSmallPet();
 
if (isFish(pet)) {
pet.swim();
}else {
pet.fly();
}
Try

Notice that TypeScript not only knows thatpet is aFish in theif branch;it also knows that in theelse branch, youdon’t have aFish, so you must have aBird.

You may use the type guardisFish to filter an array ofFish | Bird and obtain an array ofFish:

ts
constzoo: (Fish |Bird)[] = [getSmallPet(),getSmallPet(),getSmallPet()];
constunderWater1:Fish[] =zoo.filter(isFish);
// or, equivalently
constunderWater2:Fish[] =zoo.filter<Fish>(isFish);
constunderWater3:Fish[] =zoo.filter<Fish>((pet)=>isFish(pet));
Try

Using thein operator

Thein operator also acts as a narrowing expression for types.

For an in x expression, wheren is a string literal or string literal type andx is a union type, the “true” branch narrows to types which have an optional or required propertyn, and the “false” branch narrows to types which have an optional or missing propertyn.

ts
functionmove(pet:Fish |Bird) {
if ("swim"inpet) {
returnpet.swim();
}
returnpet.fly();
}
Try

typeof type guards

Let’s go back and write the code for a version ofpadLeft which uses union types.We could write it with type predicates as follows:

ts
functionisNumber(x:any):xisnumber {
returntypeofx ==="number";
}
 
functionisString(x:any):xisstring {
returntypeofx ==="string";
}
 
functionpadLeft(value:string,padding:string |number) {
if (isNumber(padding)) {
returnArray(padding +1).join(" ") +value;
}
if (isString(padding)) {
returnpadding +value;
}
thrownewError(`Expected string or number, got '${padding}'.`);
}
Try

However, having to define a function to figure out if a type is a primitive is kind of a pain.Luckily, you don’t need to abstracttypeof x === "number" into its own function because TypeScript will recognize it as a type guard on its own.That means we could just write these checks inline.

ts
functionpadLeft(value:string,padding:string |number) {
if (typeofpadding ==="number") {
returnArray(padding +1).join(" ") +value;
}
if (typeofpadding ==="string") {
returnpadding +value;
}
thrownewError(`Expected string or number, got '${padding}'.`);
}
Try

Thesetypeof type guards are recognized in two different forms:typeof v === "typename" andtypeof v !== "typename", where"typename" can be one oftypeof operator’s return values ("undefined","number","string","boolean","bigint","symbol","object", or"function").While TypeScript won’t stop you from comparing to other strings, the language won’t recognize those expressions as type guards.

instanceof type guards

If you’ve read abouttypeof type guards and are familiar with theinstanceof operator in JavaScript, you probably have some idea of what this section is about.

instanceof type guards are a way of narrowing types using their constructor function.For instance, let’s borrow our industrial strength string-padder example from earlier:

ts
interfacePadder {
getPaddingString():string;
}
 
classSpaceRepeatingPadderimplementsPadder {
constructor(privatenumSpaces:number) {}
getPaddingString() {
returnArray(this.numSpaces +1).join(" ");
}
}
 
classStringPadderimplementsPadder {
constructor(privatevalue:string) {}
getPaddingString() {
returnthis.value;
}
}
 
functiongetRandomPadder() {
returnMath.random() <0.5
?newSpaceRepeatingPadder(4)
:newStringPadder(" ");
}
 
letpadder:Padder =getRandomPadder();
let padder: Padder
 
if (padderinstanceofSpaceRepeatingPadder) {
padder;
let padder: SpaceRepeatingPadder
}
if (padderinstanceofStringPadder) {
padder;
let padder: StringPadder
}
Try

The right side of theinstanceof needs to be a constructor function, and TypeScript will narrow down to:

  1. the type of the function’sprototype property if its type is notany
  2. the union of types returned by that type’s construct signatures

in that order.

Nullable types

TypeScript has two special types,null andundefined, that have the values null and undefined respectively.We mentioned these briefly inthe Basic Types section.

By default, the type checker considersnull andundefined assignable to anything.Effectively,null andundefined are valid values of every type.That means it’s not possible tostop them from being assigned to any type, even when you would like to prevent it.The inventor ofnull, Tony Hoare, calls this his“billion dollar mistake”.

ThestrictNullChecks flag fixes this: when you declare a variable, it doesn’t automatically includenull orundefined.You can include them explicitly using a union type:

ts
letexampleString ="foo";
exampleString =null;
Type 'null' is not assignable to type 'string'.2322Type 'null' is not assignable to type 'string'.
 
letstringOrNull:string |null ="bar";
stringOrNull =null;
 
stringOrNull =undefined;
Type 'undefined' is not assignable to type 'string | null'.2322Type 'undefined' is not assignable to type 'string | null'.
Try

Note that TypeScript treatsnull andundefined differently in order to match JavaScript semantics.string | null is a different type thanstring | undefined andstring | undefined | null.

From TypeScript 3.7 and onwards, you can useoptional chaining to simplify working with nullable types.

Optional parameters and properties

WithstrictNullChecks, an optional parameter automatically adds| undefined:

ts
functionf(x:number,y?:number) {
returnx + (y ??0);
}
 
f(1,2);
f(1);
f(1,undefined);
f(1,null);
Argument of type 'null' is not assignable to parameter of type 'number | undefined'.2345Argument of type 'null' is not assignable to parameter of type 'number | undefined'.
Try

The same is true for optional properties:

ts
classC {
a:number;
b?:number;
}
 
letc =newC();
 
c.a =12;
c.a =undefined;
Type 'undefined' is not assignable to type 'number'.2322Type 'undefined' is not assignable to type 'number'.
c.b =13;
c.b =undefined;
c.b =null;
Type 'null' is not assignable to type 'number | undefined'.2322Type 'null' is not assignable to type 'number | undefined'.
Try

Type guards and type assertions

Since nullable types are implemented with a union, you need to use a type guard to get rid of thenull.Fortunately, this is the same code you’d write in #"default";

}else {
returnstringOrNull;
}
}
Try

Thenull elimination is pretty obvious here, but you can use terser operators too:

ts
functionf(stringOrNull:string |null):string {
returnstringOrNull ??"default";
}
Try

In cases where the compiler can’t eliminatenull orundefined, you can use the type assertion operator to manually remove them.The syntax is postfix!:identifier! removesnull andundefined from the type ofidentifier:

ts
interfaceUserAccount {
id:number;
email?:string;
}
 
constuser =getUser("admin");
user.id;
'user' is possibly 'undefined'.18048'user' is possibly 'undefined'.
 
if (user) {
user.email.length;
'user.email' is possibly 'undefined'.18048'user.email' is possibly 'undefined'.
}
 
// Instead if you are sure that these objects or fields exist, the
// postfix ! lets you short circuit the nullability
user!.email!.length;
Try

Type Aliases

Type aliases create a new name for a type.Type aliases are sometimes similar to interfaces, but can name primitives, unions, tuples, and any other types that you’d otherwise have to write by hand.

ts
typeSecond =number;
 
lettimeInSecond:number =10;
lettime:Second =10;
Try

Aliasing doesn’t actually create a new type - it creates a newname to refer to that type.Aliasing a primitive is not terribly useful, though it can be used as a form of documentation.

Just like interfaces, type aliases can also be generic - we can just add type parameters and use them on the right side of the alias declaration:

ts
typeContainer<T> = {value:T };

We can also have a type alias refer to itself in a property:

ts
typeTree<T> = {
value:T;
left?:Tree<T>;
right?:Tree<T>;
};

Together withintersection types, we can make some pretty mind-bending types:

ts
typeLinkedList<Type> =Type & {next:LinkedList<Type> };
 
interfacePerson {
name:string;
}
 
letpeople =getDriversLicenseQueue();
people.name;
people.next.name;
people.next.next.name;
people.next.next.next.name;
(property) next: LinkedList<Person>
Try

Interfaces vs. Type Aliases

As we mentioned, type aliases can act sort of like interfaces; however, there are some subtle differences.

Almost all features of aninterface are available intype, the key distinction is that a type cannot be re-opened to add new properties vs an interface which is always extendable.

InterfaceType

Extending an interface

interface Animal {  name: string}
interface Bear extends Animal { honey: boolean}
const bear = getBear() bear.namebear.honey

Extending a type via intersections

type Animal = {  name: string}
type Bear = Animal & { honey: Boolean }
const bear = getBear();bear.name;bear.honey;

Adding new fields to an existing interface

interface Window {  title: string}
interface Window { ts: import("typescript")}
const src = 'const a = "Hello World"';window.ts.transpileModule(src, {});

A type cannot be changed after being created

type Window = {  title: string}
type Window = { ts: import("typescript")}
// Error: Duplicate identifier 'Window'.

Because an interface more closely maps how JavaScript objects workby being open to extension, we recommend using an interface over a type alias when possible.

On the other hand, if you can’t express some shape with an interface and you need to use a union or tuple type, type aliases are usually the way to go.

Enum Member Types

As mentioned inour section on enums, enum members have types when every member is literal-initialized.

Much of the time when we talk about “singleton types”, we’re referring to both enum member types as well as numeric/string literal types, though many users will use “singleton types” and “literal types” interchangeably.

Polymorphicthis types

A polymorphicthis type represents a type that is thesubtype of the containing class or interface.This is calledF-bounded polymorphism, a lot of people know it as thefluent API pattern.This makes hierarchical fluent interfaces much easier to express, for example.Take a simple calculator that returnsthis after each operation:

ts
classBasicCalculator {
publicconstructor(protectedvalue:number =0) {}
publiccurrentValue():number {
returnthis.value;
}
publicadd(operand:number):this {
this.value +=operand;
returnthis;
}
publicmultiply(operand:number):this {
this.value *=operand;
returnthis;
}
// ... other operations go here ...
}
 
letv =newBasicCalculator(2).multiply(5).add(1).currentValue();
Try

Since the class usesthis types, you can extend it and the new class can use the old methods with no changes.

ts
classScientificCalculatorextendsBasicCalculator {
publicconstructor(value =0) {
super(value);
}
publicsin() {
this.value =Math.sin(this.value);
returnthis;
}
// ... other operations go here ...
}
 
letv =newScientificCalculator(2).multiply(5).sin().add(1).currentValue();
Try

Withoutthis types,ScientificCalculator would not have been able to extendBasicCalculator and keep the fluent interface.multiply would have returnedBasicCalculator, which doesn’t have thesin method.However, withthis types,multiply returnsthis, which isScientificCalculator here.

Index types

With index types, you can get the compiler to check code that uses dynamic property names.For example, a common JavaScript pattern is to pick a subset of properties from an object:

js
functionpluck(o,propertyNames) {
returnpropertyNames.map((n)=>o[n]);
}

Here’s how you would write and use this function in TypeScript, using theindex type query andindexed access operators:

ts
functionpluck<T,KextendskeyofT>(o:T,propertyNames:K[]):T[K][] {
returnpropertyNames.map((n)=>o[n]);
}
 
interfaceCar {
manufacturer:string;
model:string;
year:number;
}
 
lettaxi:Car = {
manufacturer:"Toyota",
model:"Camry",
year:2014,
};
 
// Manufacturer and model are both of type string,
// so we can pluck them both into a typed string array
letmakeAndModel:string[] =pluck(taxi, ["manufacturer","model"]);
 
// If we try to pluck model and year, we get an
// array of a union type: (string | number)[]
letmodelYear =pluck(taxi, ["model","year"]);
Try

The compiler checks thatmanufacturer andmodel are actually properties onCar.The example introduces a couple of new type operators.First iskeyof T, theindex type query operator.For any typeT,keyof T is the union of known, public property names ofT.For example:

ts
letcarProps:keyofCar;
let carProps: keyof Car
Try

keyof Car is completely interchangeable with"manufacturer" | "model" | "year".The difference is that if you add another property toCar, sayownersAddress: string, thenkeyof Car will automatically update to be"manufacturer" | "model" | "year" | "ownersAddress".And you can usekeyof in generic contexts likepluck, where you can’t possibly know the property names ahead of time.That means the compiler will check that you pass the right set of property names topluck:

ts
// error, Type '"unknown"' is not assignable to type '"manufacturer" | "model" | "year"'
pluck(taxi, ["year","unknown"]);

The second operator isT[K], theindexed access operator.Here, the type syntax reflects the expression syntax.That means thattaxi["manufacturer"] has the typeCar["manufacturer"] — which in our example is juststring.However, just like index type queries, you can useT[K] in a generic context, which is where its real power comes to life.You just have to make sure that the type variableK extends keyof T.Here’s another example with a function namedgetProperty.

ts
functiongetProperty<T,KextendskeyofT>(o:T,propertyName:K):T[K] {
returno[propertyName];// o[propertyName] is of type T[K]
}

IngetProperty,o: T andpropertyName: K, so that meanso[propertyName]: T[K].Once you return theT[K] result, the compiler will instantiate the actual type of the key, so the return type ofgetProperty will vary according to which property you request.

ts
letmanufacturer:string =getProperty(taxi,"manufacturer");
letyear:number =getProperty(taxi,"year");
 
letunknown =getProperty(taxi,"unknown");
Argument of type '"unknown"' is not assignable to parameter of type 'keyof Car'.2345Argument of type '"unknown"' is not assignable to parameter of type 'keyof Car'.
Try

Index types and index signatures

keyof andT[K] interact with index signatures. An index signature parameter type must be ‘string’ or ‘number’.If you have a type with a string index signature,keyof T will bestring | number(and not juststring, since in JavaScript you can access an object property eitherby using strings (object["42"]) or numbers (object[42])).AndT[string] is just the type of the index signature:

ts
interfaceDictionary<T> {
[key:string]:T;
}
letkeys:keyofDictionary<number>;
let keys: string | number
letvalue:Dictionary<number>["foo"];
let value: number
Try

If you have a type with a number index signature,keyof T will just benumber.

ts
interfaceDictionary<T> {
[key:number]:T;
}
 
letkeys:keyofDictionary<number>;
let keys: number
letnumberValue:Dictionary<number>[42];
let numberValue: number
letvalue:Dictionary<number>["foo"];
Property 'foo' does not exist on type 'Dictionary<number>'.2339Property 'foo' does not exist on type 'Dictionary<number>'.
Try

Mapped types

A common task is to take an existing type and make each of its properties optional:

ts
interfacePersonSubset {
name?:string;
age?:number;
}

Or we might want a readonly version:

ts
interfacePersonReadonly {
readonlyname:string;
readonlyage:number;
}

This happens often enough in JavaScript that TypeScript provides a way to create new types based on old types —mapped types.In a mapped type, the new type transforms each property in the old type in the same way.For example, you can make all properties optional or of a typereadonly.Here are a couple of examples:

ts
typePartial<T> = {
[PinkeyofT]?:T[P];
};
 
typeReadonly<T> = {
readonly [PinkeyofT]:T[P];
};
Try

And to use it:

ts
typePersonPartial =Partial<Person>;
type PersonPartial = { name?: string | undefined; age?: number | undefined;}
typeReadonlyPerson =Readonly<Person>;
type ReadonlyPerson = { readonly name: string; readonly age: number;}
Try

Note that this syntax describes a type rather than a member.If you want to add members, you can use an intersection type:

ts
// Use this:
typePartialWithNewMember<T> = {
[PinkeyofT]?:T[P];
} & {newMember:boolean }
 
// This is an error!
typeWrongPartialWithNewMember<T> = {
[PinkeyofT]?:T[P];
newMember:boolean;
A mapped type may not declare properties or methods.7061A mapped type may not declare properties or methods.
}
Try

Let’s take a look at the simplest mapped type and its parts:

ts
typeKeys ="option1" |"option2";
typeFlags = { [KinKeys]:boolean };
Try

The syntax resembles the syntax for index signatures with afor .. in inside.There are three parts:

  1. The type variableK, which gets bound to each property in turn.
  2. The string literal unionKeys, which contains the names of properties to iterate over.
  3. The resulting type of the property.

In this simple example,Keys is a hard-coded list of property names and the property type is alwaysboolean, so this mapped type is equivalent to writing:

ts
typeFlags = {
option1:boolean;
option2:boolean;
};
Try

Real applications, however, look likeReadonly orPartial above.They’re based on some existing type, and they transform the properties in some way.That’s wherekeyof and indexed access types come in:

ts
typeNullablePerson = { [PinkeyofPerson]:Person[P] |null };
type NullablePerson = { name: string | null; age: number | null;}
typePartialPerson = { [PinkeyofPerson]?:Person[P] };
type PartialPerson = { name?: string | undefined; age?: number | undefined;}
Try

But it’s more useful to have a general version.

ts
typeNullable<T> = { [PinkeyofT]:T[P] |null };
typePartial<T> = { [PinkeyofT]?:T[P] };

In these examples, the properties list iskeyof T and the resulting type is some variant ofT[P].This is a good template for any general use of mapped types.That’s because this kind of transformation ishomomorphic, which means that the mapping applies only to properties ofT and no others.The compiler knows that it can copy all the existing property modifiers before adding any new ones.For example, ifPerson.name was readonly,Partial<Person>.name would be readonly and optional.

Here’s one more example, in whichT[P] is wrapped in aProxy<T> class:

ts
typeProxy<T> = {
get():T;
set(value:T):void;
};
 
typeProxify<T> = {
[PinkeyofT]:Proxy<T[P]>;
};
 
functionproxify<T>(o:T):Proxify<T> {
// ... wrap proxies ...
}
 
letprops = {rooms:4 };
letproxyProps =proxify(props);
let proxyProps: Proxify<{ rooms: number;}>
Try

Note thatReadonly<T> andPartial<T> are so useful, they are included in TypeScript’s standard library along withPick andRecord:

ts
typePick<T,KextendskeyofT> = {
[PinK]:T[P];
};
typeRecord<Kextendskeyofany,T> = {
[PinK]:T;
};

Readonly,Partial andPick are homomorphic whereasRecord is not.One clue thatRecord is not homomorphic is that it doesn’t take an input type to copy properties from:

ts
typeThreeStringProps =Record<"prop1" |"prop2" |"prop3",string>;
Try

Non-homomorphic types are essentially creating new properties, so they can’t copy property modifiers from anywhere.

Note thatkeyof any represents the type of any value that can be used as an index to an object. In otherwords,keyof any is currently equal tostring | number | symbol.

Inference from mapped types

Now that you know how to wrap the properties of a type, the next thing you’ll want to do is unwrap them.Fortunately, that’s pretty easy:

ts
functionunproxify<T>(t:Proxify<T>):T {
letresult = {}asT;
for (constkint) {
result[k] =t[k].get();
}
returnresult;
}
 
letoriginalProps =unproxify(proxyProps);
let originalProps: { rooms: number;}
Try

Note that this unwrapping inference only works on homomorphic mapped types.If the mapped type is not homomorphic you’ll have to give an explicit type parameter to your unwrapping function.

Conditional Types

A conditional type selects one of two possible types based on a condition expressed as a type relationship test:

ts
TextendsU ?X :Y

The type above means whenT is assignable toU the type isX, otherwise the type isY.

A conditional typeT extends U ? X : Y is eitherresolved toX orY, ordeferred because the condition depends on one or more type variables.WhenT orU contains type variables, whether to resolve toX orY, or to defer, is determined by whether or not the type system has enough information to conclude thatT is always assignable toU.

As an example of some types that are immediately resolved, we can take a look at the following example:

ts
declarefunctionf<Textendsboolean>(x:T):Textendstrue ?string :number;
 
// Type is 'string | number'
letx =f(Math.random() <0.5);
let x: string | number
Try

Another example would be theTypeName type alias, which uses nested conditional types:

ts
typeTypeName<T> =Textendsstring
?"string"
:Textendsnumber
?"number"
:Textendsboolean
?"boolean"
:Textendsundefined
?"undefined"
:TextendsFunction
?"function"
:"object";
 
typeT0 =TypeName<string>;
type T0 = "string"
typeT1 =TypeName<"a">;
type T1 = "string"
typeT2 =TypeName<true>;
type T2 = "boolean"
typeT3 =TypeName<()=>void>;
type T3 = "function"
typeT4 =TypeName<string[]>;
type T4 = "object"
Try

But as an example of a place where conditional types are deferred - where they stick around instead of picking a branch - would be in the following:

ts
interfaceFoo {
propA:boolean;
propB:boolean;
}
 
declarefunctionf<T>(x:T):TextendsFoo ?string :number;
 
functionfoo<U>(x:U) {
// Has type 'U extends Foo ? string : number'
leta =f(x);
 
// This assignment is allowed though!
letb:string |number =a;
}
Try

In the above, the variablea has a conditional type that hasn’t yet chosen a branch.When another piece of code ends up callingfoo, it will substitute inU with some other type, and TypeScript will re-evaluate the conditional type, deciding whether it can actually pick a branch.

In the meantime, we can assign a conditional type to any other target type as long as each branch of the conditional is assignable to that target.So in our example above we were able to assignU extends Foo ? string : number tostring | number since no matter what the conditional evaluates to, it’s known to be eitherstring ornumber.

Distributive conditional types

Conditional types in which the checked type is a naked type parameter are calleddistributive conditional types.Distributive conditional types are automatically distributed over union types during instantiation.For example, an instantiation ofT extends U ? X : Y with the type argumentA | B | C forT is resolved as(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y).

Example

ts
typeT5 =TypeName<string | (()=>void)>;
type T5 = "string" | "function"
typeT6 =TypeName<string |string[] |undefined>;
type T6 = "string" | "undefined" | "object"
typeT7 =TypeName<string[] |number[]>;
type T7 = "object"
Try

In instantiations of a distributive conditional typeT extends U ? X : Y, references toT within the conditional type are resolved to individual constituents of the union type (i.e.T refers to the individual constituentsafter the conditional type is distributed over the union type).Furthermore, references toT withinX have an additional type parameter constraintU (i.e.T is considered assignable toU withinX).

Example

ts
typeBoxedValue<T> = {value:T };
typeBoxedArray<T> = {array:T[] };
typeBoxed<T> =Textendsany[] ?BoxedArray<T[number]> :BoxedValue<T>;
 
typeT1 =Boxed<string>;
type T1 = { value: string;}
typeT2 =Boxed<number[]>;
type T2 = { array: number[];}
typeT3 =Boxed<string |number[]>;
type T3 = BoxedValue<string> | BoxedArray<number>
Try

Notice thatT has the additional constraintany[] within the true branch ofBoxed<T> and it is therefore possible to refer to the element type of the array asT[number]. Also, notice how the conditional type is distributed over the union type in the last example.

The distributive property of conditional types can conveniently be used tofilter union types:

ts
// Remove types from T that are assignable to U
typeDiff<T,U> =TextendsU ?never :T;
// Remove types from T that are not assignable to U
typeFilter<T,U> =TextendsU ?T :never;
 
typeT1 =Diff<"a" |"b" |"c" |"d","a" |"c" |"f">;
type T1 = "b" | "d"
typeT2 =Filter<"a" |"b" |"c" |"d","a" |"c" |"f">;// "a" | "c"
type T2 = "a" | "c"
typeT3 =Diff<string |number | (()=>void),Function>;// string | number
type T3 = string | number
typeT4 =Filter<string |number | (()=>void),Function>;// () => void
type T4 = () => void
 
// Remove null and undefined from T
typeNotNullable<T> =Diff<T,null |undefined>;
 
typeT5 =NotNullable<string |number |undefined>;
type T5 = string | number
typeT6 =NotNullable<string |string[] |null |undefined>;
type T6 = string | string[]
 
functionf1<T>(x:T,y:NotNullable<T>) {
x =y;
y =x;
Type 'T' is not assignable to type 'Diff<T, null | undefined>'.2322Type 'T' is not assignable to type 'Diff<T, null | undefined>'.
}
 
functionf2<Textendsstring |undefined>(x:T,y:NotNullable<T>) {
x =y;
y =x;
Type 'T' is not assignable to type 'Diff<T, null | undefined>'. Type 'string | undefined' is not assignable to type 'Diff<T, null | undefined>'. Type 'undefined' is not assignable to type 'Diff<T, null | undefined>'.2322Type 'T' is not assignable to type 'Diff<T, null | undefined>'. Type 'string | undefined' is not assignable to type 'Diff<T, null | undefined>'. Type 'undefined' is not assignable to type 'Diff<T, null | undefined>'.
lets1:string =x;
lets2:string =y;
}
Try

Conditional types are particularly useful when combined with mapped types:

ts
typeFunctionPropertyNames<T> = {
[KinkeyofT]:T[K]extendsFunction ?K :never;
}[keyofT];
typeFunctionProperties<T> =Pick<T,FunctionPropertyNames<T>>;
 
typeNonFunctionPropertyNames<T> = {
[KinkeyofT]:T[K]extendsFunction ?never :K;
}[keyofT];
typeNonFunctionProperties<T> =Pick<T,NonFunctionPropertyNames<T>>;
 
interfacePart {
id:number;
name:string;
subparts:Part[];
updatePart(newName:string):void;
}
 
typeT1 =FunctionPropertyNames<Part>;
type T1 = "updatePart"
typeT2 =NonFunctionPropertyNames<Part>;
type T2 = "id" | "name" | "subparts"
typeT3 =FunctionProperties<Part>;
type T3 = { updatePart: (newName: string) => void;}
typeT4 =NonFunctionProperties<Part>;
type T4 = { id: number; name: string; subparts: Part[];}
Try

Note, conditional types are not permitted to reference themselves recursively. For example the following is an error.

Example

ts
typeElementType<T> =Textendsany[] ?ElementType<T[number]> :T;// Error
Try

Type inference in conditional types

Within theextends clause of a conditional type, it is now possible to haveinfer declarations that introduce a type variable to be inferred.Such inferred type variables may be referenced in the true branch of the conditional type.It is possible to have multipleinfer locations for the same type variable.

For example, the following extracts the return type of a function type:

ts
typeReturnType<T> =Textends (...args:any[])=>inferR ?R :any;
Try

Conditional types can be nested to form a sequence of pattern matches that are evaluated in order:

ts
typeUnpacked<T> =Textends (inferU)[]
?U
:Textends (...args:any[])=>inferU
?U
:TextendsPromise<inferU>
?U
:T;
 
typeT0 =Unpacked<string>;
type T0 = string
typeT1 =Unpacked<string[]>;
type T1 = string
typeT2 =Unpacked<()=>string>;
type T2 = string
typeT3 =Unpacked<Promise<string>>;
type T3 = string
typeT4 =Unpacked<Promise<string>[]>;
type T4 = Promise<string>
typeT5 =Unpacked<Unpacked<Promise<string>[]>>;
type T5 = string
Try

The following example demonstrates how multiple candidates for the same type variable in co-variant positions causes a union type to be inferred:

ts
typeFoo<T> =Textends {a:inferU;b:inferU } ?U :never;
 
typeT1 =Foo<{a:string;b:string }>;
type T1 = string
typeT2 =Foo<{a:string;b:number }>;
type T2 = string | number
Try

Likewise, multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred:

ts
typeBar<T> =Textends {a: (x:inferU)=>void;b: (x:inferU)=>void }
?U
:never;
 
typeT1 =Bar<{a: (x:string)=>void;b: (x:string)=>void }>;
type T1 = string
typeT2 =Bar<{a: (x:string)=>void;b: (x:number)=>void }>;
type T2 = never
Try

When inferring from a type with multiple call signatures (such as the type of an overloaded function), inferences are made from thelast signature (which, presumably, is the most permissive catch-all case).It is not possible to perform overload resolution based on a list of argument types.

ts
declarefunctionfoo(x:string):number;
declarefunctionfoo(x:number):string;
declarefunctionfoo(x:string |number):string |number;
 
typeT1 =ReturnType<typeoffoo>;
type T1 = string | number
Try

It is not possible to useinfer declarations in constraint clauses for regular type parameters:

ts
typeReturnedType<Textends (...args:any[])=>inferR> =R;
'infer' declarations are only permitted in the 'extends' clause of a conditional type.
Cannot find name 'R'.
1338
2304
'infer' declarations are only permitted in the 'extends' clause of a conditional type.
Cannot find name 'R'.
Try

However, much the same effect can be obtained by erasing the type variables in the constraint and instead specifying a conditional type:

ts
typeAnyFunction = (...args:any[])=>any;
typeReturnType<TextendsAnyFunction> =Textends (...args:any[])=>inferR
?R
:any;
Try

Predefined conditional types

TypeScript adds several predefined conditional types, you can find the full list and examples inUtility Types.