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!