Aloha.zone.io

Aloha's technical notes.

View on GitHub

Type System

TypeScript has a real cool and strong type system. Let’s make good use of it.


Infer

The keyword infer one of the most powerful keywords for building types, which helps us to extract any type from any nested types.

Example, extract the type list of the parameters of a function

type Param<T extends (...args: any[]) => any> = 
  T extends (...args: infer A)  => any
  ? A
  : never

const foo = (a: number, b: string) => true
const bar = (a: boolean, b: number, c: number) => false
type t1 = Param<typeof foo>   // [number, string]
type t2 = Param<typeof bar>   // [boolean, number, number]



Type Constructor / Type Utilities

TypeScript allows builds new types from old ones and define some type infer to generate types from existing types.

It is a typical feature called Type Constructor in type theory.

Type constructors are really useful to use such type utilities to build more strong and reliable types.

Built-in utilities types

TypeScript contains several useful type utils,

Refer to official utility types.

Example of some really useful ones:

Third party extended type utility lib

Custom useful types

With keyword typeof, keyof, infer, … We can extend and derive almost any types from existing types.

In specific use cases, there will be some cusomized types to resovle problem:



Covariance & contravariance (协变与逆变)

Covariance and contravariance is what describes how subtyping works in a programming language.

1. A Java problem

See the example below:

    public static void f() {
        String[] a = new String[2];
        Object[] b = a;
        a[0] = "hi";
        b[1] = Integer.valueOf(42);
    }
Answer ```java public static void f() { String[] a = new String[2]; Object[] b = a; a[0] = "hi"; b[1] = Integer.valueOf(42); // <--- Runtime exception: java.lang.ArrayStoreException } ```
Why not like this? ```java public static void f() { String[] a = new String[2]; Object[] b = a; // ~~~~~~~ Compile error: "a": String[] could not be assigned to "b": Object[] a[0] = "hi"; b[1] = Integer.valueOf(42); } ```
It is Object array here, why Object is right? ```java public static void f() { String a = new String(); Object b = a; // Typical polymorphism a = "hi"; b = Integer.valueOf(42); // Awesome! } ``` What's the difference? - Array has multiple values? - `b[0]` and `b[1]` cannot have different types? - We say "String[] should not be assigned to Object[]", why "assign String to Object" is the core of OOP?
Answer - In OOP, we say String is a typically subtype of Object. - Assign subtype to super-type is always correct. - ***But, WATCH OUT! String[] is not the subtype of Object[] !***

In conclusion, we could say T[A] is subtype of T[B], regardless T’s definition but only know A is subtype of B.


2. Covariant, contravariant, bivariant and invariant

Convariance and contravariance are describing the relationship of types after a type calculation.


3. Covariance in arrays

Back to the question above, the problem comes to String[] is not subtype of Object[]?

  1. Covariance, contravariance, invariance?
  2. In Java, we see it is covariance. String[] is allowed to be assigned to Object[].

    But obviously, it’s not correct when write the array is writtable.

  3. If we say the array is read only, covariance is correct now.

    /* readonly*/ Object[] a = new String[] {"foo", "bar", "test"};
    System.out.println(a[0], a[1], a[2]);
    
Answer - A readable and writeable array should be ***INVARIANT***. That's why we say `String[]` is not subtype of `Object[]` - A readonly array is **covariant**. Instead, we can say `readonly String[]` is a subtype of `readonly Object[]`
Extension - So this is typical static typing problem in Java (as well as C#). - Guess Why?
Root cause - Yes, GENERICS. Java and C# does not support generics in old time. They use parent typing like the generic bounding to make functions accept more generic types. ```Java boolean equalArrays (Object[] a1, Object[] a2); // equal function should be readonly, which is safe. void shuffleArray(Object[] a); ``` - It should be defined like this. ```Java <T extends Comparable> boolean equalArrays (T[] a1, T[] a2); void shuffleArray(T[] a); ``` - Today, the legacy feature is a burden now. Use it must take care of if the array is writable to avoid runtime errors. Or use some immutable/readonly array instead rather a raw object array. (of course, they introduces overhead before Java/C# introdues raw immutable data type primitives) In C# : ```cs IEnumerable // replace "object[]" ``` In Java : ```Java List items = Collections.unmodifiableList(Arrays.asList("a", "b", "c")); ``` </details> </details>
#### 4. Covariance in function typing The correct behavior for function typing is: - The return type is covariant. Given `A` <: `B`, we have `() => A ` <: ` () => B`. - The parameters' types are contravariant. Given `A` <: `B`, we have `(a: A) => void ` :> ` (b: B) => void`. - Above rules work together. Given `A` <: `B` and `C` <: `D`, we have `(b: B) => C ` :> ` (a: A) => D`. See the function stype [sample](https://www.typescriptlang.org/play?ssl=36&ssc=13&pln=36&pc=18#code/PTAEFEA8EMFsAcA2BTUBaUArAltAXgBYCuAUCKAMID2AdgC4BO0AbtA7vaNDQCaVWt23OiTJgACimgBnVAGNE2OQGtQAImo0AZtgDma0HSqgiNOQWQrDF9dMZK6AMVNy62WgBUAnvGTSDRqCyqHQ2AO5sNNg0utJcutDRAHSi0XTIDFrQcqgAglGw0IigAN4koBWgFnoEdABcoDREsABGGSQAvqn0GVk5oAAiVLqgyJDpvHH52IXFZZWgLWzKDQAUAJSgALwAfKDMVNg8nd3pmdmoAOIMyF4EVKZ8YxM8cUMj85VaVFRrm7v7Q7HLokNK9C6gAASRGkyi8o3GyEmg2GpXKlSWDD+2z2ByOJ1E5C0LjctFAAAMtOTQIV4TDUNBQDxsFotBkkXRDD4GTQaFQ6NBSTR1NDYV4DGECEoCDToPCWjdoKpQqhBBwRHJaHZQFptqBViM1jxhg13v89maGk1WhkcWiFpqaNriGK9SUOlw4qK4eiKjc6EQGMLdKsXXD1klqrpagSSI7tboAIx60MPXgNa63e6PdYZm53NN8AElUD+wPCktR2oNRMABlAHpkoEzBceDdE8c5ugATCns+nm-n+zxc6BprM7SWy0HSlVkDV6qA6w3PWOCkV23GtV2AMwp7gzIoNcdFUcn4rF0vIAMzyvz6OL5eNqbr4ogzugXQAFn3r+Pr9HFth0nK8bwrOcFxretn0HLNC03EgtANRNNlAcgAA1RCQntUIwrCDR3XCwEwxCDS-IjQEALCISCAA), and [React FC sample](https://github.com/GarfieldZHU/Aloha.zone.io/blob/master/TypeScript/type_system.md#7-samples) TS config `strictFunctionType` will control if the function parameter is acknowledged as covariance or contravariance.
#### 5. Covariance in inheritance In OO languages (cpp, Java, C#, etc.), **OVERRIDE** is key concept in subclass to implement a different method from super class. We know that overriding should have the same mehtod signature, but it also allows **covariance** in some languages. - Covariant method return type In function part, we already know that return type is covariant ( Given `A` <: `B`, we have `() => A ` <: ` () => B`. ) In inheritance, overriding a method with its subtype method is a covariance in heritance. (Java and C++ support this, C# does not) ```Java class Animal { Animal getAnimal() { // ... } } // Child class class Cat extends Animal { @overrides Cat getAnimal() { ... } } ``` - Contravariant method parameter type Like the above section, we can guess that contravariant in method parameter type is also a type-safe overriding. **YES. IT IS TYPE SAFE.** But languages rarely implement it. 😅 In Java, C++, C#, it will be regarded as overloading instead of a overriding. ```Java class Animal { void setAnimal(Animal a) { // ... } } // Child class class Cat extends Animal { // It's still correct. But it's not overriding. It's a overloading in Java. // This method could not be hit by a call of "cat.setAnimal(animal)" unless `animal` is not an instance of Animal. void setAnimal(Object a) { // ... } } ```
#### 6. Covariance in generic There are two main approaches for generic type: - Declaration-site variance annotations (C#) - Use-site variance annotations (Java) Conclude from the above sections, we may found that: 1) The generic for type of input parameters, should be covariant. 2) The generic for type of output parameters, should be contravariant. ##### Declaration-site variance annotations C# uses keyword `in`(covariant) and `out`(contravariant) to mark the types. ```cs interface IEnumerator { T Current { get; } bool MoveNext(); } ``` It will report error when declare the interface with using `out T` as type of an input parameter. Scala uses `+`(covariant) and `-`(contravariant) as keywords. ```scala sealed abstract class List[+A] extends AbstractSeq[A] { def head: A def tail: List[A] /** Adds an element at the beginning of this list. */ def ::[B >: A] (x: B): List[B] = new scala.collection.immutable.::(x, this) /** ... */ } ``` ##### Use-site variance annotations It checks the variance covariance when generic type is instantiated. Given `type A = T`, it should reports error when instantiated a `A` when `Test` does not match `T`'s requirement. A typical implementation is "**upper/lower boundary constraints**" 1. In Java We have bound descriptor `extends` and `super` in Java ```Java // Lower bounds is very common in the languages support generic List<? extends Animal> // Upper bounds is not common, Java uses "super" keyword List<? super Animal> ``` 2. In Typescript Up-to-date, Typescript does not have a upper-bound generic type contraints yet. The open issue is: [TypeScript#9252](https://github.com/microsoft/TypeScript/issues/9252) However, with the existing TS type utilies, we have a workaround to support upper boundary: say `Partial` See the case discussed: [here](https://github.com/Microsoft/TypeScript/issues/4889#issuecomment-200388292), think: `Partial` is equivalent with `` ?
#### 7. Samples - In React component React functional component is recommended. The variance of React props should be understood to avoid unnecessary problems. See the example below in real practice, where is the problem? ```typescript // React component definition: type Elem = { id: string; } type Props = { elem: Elem; generate: () => Elem; onClick: (elem: Elem) => void; } // React FC const MyComp: React.FC = (props: Props) => { const { elem, onClick, } const guiElement = { ...elem, // Extended GUI properties name: `element - ${elem.id}`, desc: `description for - ${elem.id}`, } // Event handler callback const clickHandler = React.useCallback((_e) => { console.log(generate()) onClick(guiElement) }, []) return <button onClick={clickHandler}> {guiElement.id} </button> } ``` ```typescript // Use the above component const customElem = { id: '0hd3ga1fa3h2664g', count: 99, name: 'bar', } const g = () => customElem const cb = (e: typeof customElem) => { alert((e as any).name) // ? alert(JSON.stringify(e)) // ? alert(e.name.split(' ')) // ? } ReactDOM.render(<MyComp elem={customElem} // is this correct? generate={g} // is this correct? onClick={cb} // is this correct? />) ``` Simplify it, look at: [Sample on playground](https://www.typescriptlang.org/play?noUncheckedIndexedAccess=true&downlevelIteration=true&noUnusedLocals=true&noUnusedParameters=true&importHelpers=true#code/PTAEBEFMDMEsDtYBdYHt4CgFMgJ2gIYDGkoAogDaQC2oA3hqE6LACYBcoAzkrggOYBuDAF8s8HPmKkAwgFceqapRqhIADxzxWXclVoNmoeAWqROPPvCGjxkwiVABxObBW0NWnXtWHmrSC4iC14BYTEMInQeUAAlSGIkGSUAB1AAXlAACkZmSH1OdwAaUFymfkh4PAIcTiyASgyAPh9qIrLQIgAjOvyaQv1G9JaAN1Q2dqGWvyYo+Bj+V3cM0Aqq3BrIBo65rlQqADoKVH4sxbdBna6svup62wwMEFAAVS4CCsjopDUC0HlFMp9CsZiwOKAAOQARhhUIh7SMJjMnAhACZ0ej4Q9dj90DIABYEazmJg3P4uC40KagMZsRiZMn9ZxLQbNeg7aL7SBHE5ZCEtABWe3gKJKACkAMoAeQAcgdLAJYNAAJ6Mu73IzPAAqqFAXVwCQA1qAkPjYLoAO74yqdY5cAQm62gAAGCqISAAYnJ4O60PAtcqUpBnZ1rURDRz5lyeadbgcAkEDqFqA17hEcatKtVatlqRTlhgGdTQc8nFmNjhQARnC8AJK-GiVJAdA1IOS4eDsoxGNgoghdIhY7v+QLBSFzLRIIfMMQRZ7xACOrgNoAARMVh6TqRujGqBlSjMMaeNWKunmAnLARjamKuAUglDuGmz8-oStlbpxXwej7TT+fQAAWlAABBLh7X4Ts7wUB8gRoVdN03B813cBDEMQ81OlQXADXdA4AOAsCIKg58j2-ag0PQ5DV1IlpUPQphMKiHDIDwgjQPA2BILXPdmUpO42T-BDqN49xqSElhLVwdB+Hw+JEmSagUjVEo1mzSASjxQliXuIA) - Sub union as React props in TypeScript Given a React component requires a prop with a union type: A | B | C, the user uses the component provides an instance with type of the narrower union type: A | C. Is this safe? ```typescript type Props = { bar: A | B | C } const Foo: React.FC = (props) => { // ... do with props.bar } // use case: type TestType = A | C const myBar: TestType render(<Foo bar={myBar} />) // ?? Type safe ``` - Remember runtime features will corrupt your static typing. Like **Reflection**, it will make the type inference missing at where the reflection begins. Reflection is the concept occurs in Java and C#. For dynamic language, it is more common being used naturally with literal objects. Like `for (const key in obj) ...` It is really useful, but it is really a runtime feature and heavily breaks static typing system. Remember use it where you understand and be careful with typing. Example: ```typescript // Base type type Base = { id: string; name: string; } // literal object derives the `Base` const obj: Base = { id: 'abc', name: 'test', category: 'foo', item: 'bar', } // JSON stringify method will iterate the runtime instance of `obj`, instead of the part of `Base` const json = JSON.stringify(obj) // JSON.parse is also runtime method, which makes us lost typing information. const restored = JSON.parse(json) /* as Base */ ```