Poznajemy nowy utility type TSa, czyli NotNullable plus całą teorię dlaczego akurat to tak działa. Po tej lekcji będziemy w stanie napisać własny NonNullable i rozumieć, dlaczego to tak działa.

Ok, oto przykład:

type T0 = NonNullable<string | number | undefined>;
//type T0 = string | number

Dobra, fajnie. Teraz z nullem:

type T1 = NonNullable<string[] | null | undefined>;
//type T1 = string[]

Jak widać tylko to, co nie jest nullem bądź undefinedem przechodzi. Niedawno poznaliśmy never i jak ono działa w union types, ale z null i undefined jeszcze chyba za bardzo przyjemności nie mieliśmy się poznać.

Ok, fajnie, ale jak to działa? Oto definicja NonNullable:

type NonNullable<T> = T & {}

Pewnie szału można dostać, jak się to widzi. O co tu chodzi? Ok zobaczmy takie coś:

let emptyArr = [];
let emptyObj = {};
let justNull = null;

type emptyObjType = typeof emptyObj;
// type emptyObjType = {}

type justNullType = typeof justNull;
// type justNullType = null

type emptyArrType = typeof emptyArr;
// type emptyArrType = never[]

Ok, drążymy temat dalej:

type trueorfalse<T extends {} > = T;

type sth = trueorfalse<null>
// Type 'null' does not satisfy the constraint '{}'.ts(2344)

type sth2 = trueorfalse<undefined>
//Type 'undefined' does not satisfy the constraint '{}'.ts(2344)

Jak widać taki constraint wywala nam nulle i undefined z równania. Pytanie, czy inne typy się zachowają:

type trueorfalse<T extends {} > = T;

type sth = trueorfalse<{}> //ok
type sth2 = trueorfalse<[]> //ok
type sth3 = trueorfalse<"hello"> //ok
type sth4 = trueorfalse<1000> //ok

Jak widać działa. Zobaczmy raz jeszcze jak to wygląda:

type NonNullable<T> = T & {}

Bierzemy każdy typ i zwracamy ten typ, ale musi wypełniać constraint {}. Tylko null i undefined go nie wypełniają.

Zauważmy, że pusty literał obiektu to jest dużo luźniejszy constraint niż object:

type trueorfalse<T extends object > = T;

type sth = trueorfalse<{}> //ok
type sth2 = trueorfalse<[]> //ok
type sth3 = trueorfalse<"hello">
//Type 'string' does not satisfy the constraint 'object'.ts(2344)
type sth4 = trueorfalse<1000>
//Type 'string' does not satisfy the constraint 'object'.ts(2344)
type sth5 = trueorfalse<null>
//Type 'null' does not satisfy the constraint 'object'.ts(2344)
type sth6 = trueorfalse<undefined>
//Type 'undefined' does not satisfy the constraint 'object'.ts(2344)

Czyli object to musi być typ złożony/referencyjny. Zaś literał {} to wszystko, co nie jest nullem ani undefinedem.

Ok, możemy się jeszcze typ zabawić:

type IsValueType<T extends {}> =  T extends object ? false: true;

type sth = IsValueType<{}>
// type sth = false

type sth2 = IsValueType<[]>
// type sth2 = false

type sth3 = IsValueType<"">
//type sth3 = true

type sth4 = IsValueType<123>
//type sth4 = true

Oczywiście bardziej normalnie by to tak wyglądało:

type IsValueType<T extends {}> =  T extends number|string ? true: false;

type sth = IsValueType<{}>
// type sth = false

type sth2 = IsValueType<[]>
// type sth2 = false

type sth3 = IsValueType<"">
//type sth3 = true

type sth4 = IsValueType<123>
//type sth4 = true

Natomiast też chodzi o to, abyśmy rozumieli, co się dzieje. Więcej TSa niedługo!