Typescript: a place where old hands are easily confused

Time:2022-6-4

First of all, it should be noted that because the typescript type system ultimately serves JavaScript, TS must be able to declare the corresponding type constraints for any code written in JS, which may lead to very complex type declarations in TS. The general strongly typed languages do not have such a problem, because at the beginning of the design, those interfaces that cannot be declared by the type system are not allowed to be created at all.

Moreover, typescript is a structured type, which is different from the nominal type. Whether any type fits depends on whether its structure fits, while the nominal type must have strict type correspondence.

Many of the following contents are based on the above two points. Let’s get to the point.

1、 About enumerations

In addition to the declaration of normal enumeration, the enumeration declaration will be different in different scenarios.

  1. Dynamic enumeration

Dynamic numeric values are allowed to be initialized in enumerations, but not strings

//Dynamic value
enum A { 
    Error = Math.random(),
        Yes = 3 * 9
}

//Cannot be used with string
enum A {
    Error = Math. Random(), // error: computed values are not allowed in enumerations containing string value members
    Yes = 'Yes',
}
  1. Add const prefix
//Enumeration with const prefix will directly replace variable references with strings in the compiler
const enum NoYes { No='No', Yes='Yes' }

//Before compilation:
const a = NoYes.NO
//After compilation:
const a = 'No'
  1. As object
//Because TS is a structural type system, enumeration can also be passed in as an object, but it always feels strange
enum NoYes {
  No = 'No',
  Yes = 'Yes',
}
function func(obj: { No: string }) {
    return obj.No;
}
func(NoYes); //  Compilation passed
  1. Numeric and string enumerations have different checking looseness
//Loose checking of numeric enumerations
enum NoYes { No, Yes }
function func(noYes: NoYes) {}
func(33); //  No type error will be reported!

//String enumeration but error
enum NoYes { No='No', Yes='Yes' }
function func(noYes: NoYes) {}
func('NO'); //  Error: a parameter of type 'no' cannot be assigned to a parameter of type 'Noyes'

I guess the reason why we allow numbers to be assigned to enumerations at will is also because we allow dynamic numerical enumerations.

2、 Why can’t overloads be written separately

Function overloading in TS:

function foo(p: string);
function foo(p: number);
function foo(p: string | number) { ... };

Overloads in Java:

public class Overloading {
    public int foo(){
        System.out.println("test1");
        return 1;
    }
 
    public void foo(int a){
        System.out.println("test2");
    }   
}

Separate write overloads are not supported because:

  • The traditional overloaded function is to split the overloaded function into func1 and func2 at compile time, and then modify the name at the call, so as to achieve the effect of distinguishing calls by parameters. JavaScript can modify the type at any time during runtime. If the traditional overloaded Compilation Rules are still used, unexpected problems may occur.
  • The interactivity between ts and JS is affected. If functions are split like traditional overloads, there will be problems calling overloaded methods in TS in JS scripts.

Therefore, compared with traditional overloading, TS overloading is more like a type annotation.

3、 Why any

Imagine a scenario in which a function needs to accept an array. The data in the array can be of any type. It is impossible to solve this problem perfectly if you want to use generics, so you have to introduce any, and unkown was later introduced to replace any. However, general strongly typed languages usually do not have so much flexibility. For example, arrays only allow one type, which can be solved through generics.

Typescript: a place where old hands are easily confused

JSON. The type declaration of parse is also any, because there is no unkown at the beginning, otherwise it is more reasonable to return unkown.

4、 Turing complete type system

Typescript brings Turing’s complete type system in order not to weaken the flexibility of JavaScript, but also to provide sufficient type constraints.

The following uses types to implement an automatic declaration of n-length tuples. Recursion is required in the process

type ToArr<N, Arr extends number[] = []>
    =Arr['length'] extends n // judge whether the array length reaches
                ?  Arr // if the length is enough, it will be returned directly
                : ToArr<N, [...Arr, number]>; //  Recursion if the length is not enough

type Arr1 = ToArr<3>; // [number, number, number]

Furthermore, addition can even be realized based on toarr above:

type Add<A extends number, B extends number> = [...ToArr<A>, ...ToArr<B>]['length'];
type Res = Add<3, 4>; // 7

Some people even use the TS type system to implement chess rules.

5、 Readonly and as const

Both readonly and as const can declare a type asReadable only, and as const can alsoConvert to constant。 Let’s take a look at some details of both.

interface Foo {
    readonly a: {
        b: number,
    },
}

const f: Foo = {
    a: { b: 1 },
};

f.a = { b: 2 }; //  Error: cannot assign to 'a' because it is read-only

f.a.b = 2; //  No problem here

As you can see, readonly is only valid for the current object, not its attributes. But readonly can make the array completely unmodifiable.

const arr: readonly number[] = [2];
arr.push(1); //  Error: attribute 'push' does not exist on type 'readonly number[]'

As const converts an array into a meta group, and converts a variable length array declaration into a fixed length array:

const args = [8, 5]; // number[]
const func = (x: number, y: number) => {};
const angle = func(...args); //  An error will be prompted here because the TS is not sure whether args has two numbers

const args = [8, 5] as const; //  Add as const to convert args to [number, number]
args. Push (2) // error: attribute "push" does not exist on type "readonly [8, 5]"

6、 Type constraint reset

The narrowed type constraint in the callback function will be reset, because the callback may be called after the asynchronous code, and the variables accessed through the closure may be changed, so the constraint reset is reasonable.

See the following examples for details:

type MyType = {
    prop?: number | string,
};
function func(arg: MyType) {
    if (typeof arg.prop === 'string') {
        const a = arg.prop; // string

        [].forEach(() => {
                        //If this is an asynchronous callback, rewriting the variable below will change the Arg here, so the constraint reset is reasonable
            const b = arg.prop; // string | number | undefined
            console.log(b);
        });

                (() => {
                        //Executing the function immediately does not reset the type constraint
            const d = arg.prop;  // string
            console.log(d);
        })();

                //Override variable
                arg = {};
    }
}

This is also the rigor required to cope with the flexibility of JS.

7、 Covariance and inversion

  • Covariant: the subtype is compatible with the parent type, that is, array<father> Push (son). This can be true because son is a subtype of father and inherits all the attributes of father. Therefore, it is compatible with father;
  • Contravariant: the parent type is compatible with the child type, which is the opposite of the above. See the following example for details;
declare let animalFn: (x: Animal) => void;
function walkdog(fn: (x: Dog) => void) {}
walkdog(animalFn); // OK

Here, the parameter declaration of animalfn requires dog, but the actual input is animal. The essence of the above is (x: dog) = > void = (x: animal) = > void. Therefore, the parameter assigns animal to dog, so animal is compatible with dog, that is, inverse. If you reverse the scene, you will make an error. So the parameter of the function is inverse and the return value is covariant.

However, the function type of TS is actually bidirectional covariant, but it is not safe. See the following examples for details:

declare let animalFn: (x: Animal) => void
declare let dogFn: (x: Dog) => void
Animalfn = dogfn // OK, but this is not safe
dogFn = animalFn  // OK

//Although two-way assignment (two-way covariance) can be passed in TS as above, it is not safe

const animalSpeak = (fn: AnimalFn) => {
    fn(animal); 
};
animalSpeak((x: Dog) => {
    x. Wangwang() // an error will be reported when running here because the incoming animal does not have a dog Woof woof method
});

The call to animalspeak above actually assigns animal as a parameter to dog. This does not meet the principle of function parameter inversion, but it can be compiled in TS.

Why TS allows functions to be bidirectional covariant: because TS is a structured language, if array (dog) can be assigned to array (animal), it means array (dog) Push can be assigned to array (animal) Push, which allows bidirectional covariance in design. This is a trade-off for TS designers to maintain structural type compatibility. After all, two-way covariance is not safe. Therefore, after version 2.6, if the strict mode is enabled, the function parameter covariance will report an error. For two-way covariance, see the following examples:

interface Animal { eat: '' }
interface Dog extends Animal { wang: '' }

let animalArr: Animal[] = [];
let dogArr: Dog[] = [];
 
animalArr = dogArr; // OK

// Array<Animal>. push(Animal): number = Array<Dog>. Push (dog): number (parameter covariance)
animalArr.push = dogArr.push; // OK

8、 In addition to type constraints, typescript brings more complete object-oriented

Because the complexity of part of the front end has been eliminated by the MVVM framework, and the object-oriented simulated by JS in the past is somewhat problematic, I often ignore the value of object-oriented. However, TS brings more complete and secure encapsulation, inheritance and polymorphism. So when writing ts in the future, remind yourself to think more about coding.

reference resources:
https://exploringjs.com/tackl…
https://www.zhihu.com/questio…
https://jkchao.github.io/type…
https://zhuanlan.zhihu.com/p/…

Recommended Today

Modul of fastems

Each module of fastems is implemented from the abstract class Fastems.Mms.Client.Infrastructure.UiModuleBase; public class DataManagerModule : UiModuleBase { public override void Initialize() { AddResourceDictionary(“/Resources/DataManagerResources.xaml”, typeof(DataManagerModule)); RegisterViewWithRegion(“DialogRegion”, typeof(DialogView)); RegisterViewWithRegion(“BusyIndicatorRegion”, typeof(BusyIndicatorView)); } } And Fastems.Mms.Client.Infrastructure.UiModuleBase inherits from Fastems.Mms.Client.Infrastructure.ModuleBase public abstract class UiModuleBase : ModuleBase { [Import] public IRegionManager RegionManager { get; set; } [Import] public IMergedDictionaryRegistry MergedDictionaryRegistry { […]