Typescript types and the difference between types and interfaces

[Official Document](TypeScript: JavaScript With Syntax For Types.)

> The following is a condensed version. If you don’t want to read the full article, just read the following content.

ts has 8 built-in types

  • Number
  • String
  • BigInt
  • Boolean
  • Symbol
  • Null
  • Undefined
  • Object

For more details, please refer to [MDN](JavaScript data types and data structures – JavaScript | MDN),

Note: All primitive types can be tested using typeof. Except for null, typeof null returns ‘object’ and needs to be tested using ===null.

Other important ts types

  • unKnown
  • never
  • object literal eg{property:Type}
  • void for functions with no documented return value
  • T[] Array[T]
  • [T,T] tuples,
  • (t:T) => U functions

Notes:

1. The syntax of the method contains the names of the parameters, which is difficult to use.

let fast:(a:any,b:any)=>any=(a,b)=>a;
//More accurate
let fast:<T,U>(a:T,b:U)=>T=(a,b)=>a;

2. The object type indicates the syntax of the object value.

let o:{n:number;xs:object[]} = {n:1,xs:[]};

Boxed types

Javascript has boxed equivalents of basic types that contain methods relevant to these types by programmers. Typescript reflects this, for example, the difference between the primitive type number and the boxed type Number. Boxed types are rarely needed because their methods return primitive methods

(1).toExponential();
// equivalent to
Number.prototype.toExponential.call(1);

Gradual typing

Typescript uses the type any when it cannot determine the type of an expression. Compared to Dynamic, calling any a type is an overstatement. It just turns off the type checker wherever it appears, configuring tsconfig.json with noImplicitAny

//tsconfig.json
{
  "compilerOptions": {
    "noImplicitAny": true
  }
}

Structural typing

Structural types are a concept familiar to most function programmers.

// @strict: false
let o = { x: "hi", extra: 1 }; // ok
let o2: { x: string } = o; // ok

Below you can use type naming, interfaces, and classes to define interfaces (however, type aliases behave differently than interfaces with regard to recursive definitions and type parameters)

type One = { p: string };
interface Two {
  p: string;
}
class Three {
  p = "Hello";
}
 
let x: One = { p: "hi" };
let two: Two = x;
two = new Three();

Union type (|)

A union type consists of two or more members, and when used, the value must satisfy one of them.

The following are the built-in conditions for determining union types:

  • String typeof s === “string”
  • Number typeof n ===”number”
  • Big integer typeof m === “bigint”
  • Boolean value typeof b === “boolean”
  • Symbol typeof g === “symbol”
  • undefined typeof undefined === “undefined”
  • Function typeof f === “function”
  • Array Array.isArray(a)
  • Object typeof o === “object”
type LockStates = "locked" | "unlocked"
let state:LockStates = 'locked'

//Pay attention to the use of the following content
let s = "right";
pad("hi",10,s) //At this time, s reports an error error:'string' is not assignable to '"left"' | '"right"'

//Change usage Correct usage
let s:"left" | "right" = "right";
pad("hi",10,s)

Cross type (& amp;)

Cross types can combine multiple types into one type, and the merged type has the properties and methods of all types.

type Combined = { a: number } & amp; { b: string };
type Conflicting = { a: number } & amp; { a: string };

Note: Conflicting a has two types, one is number and the other is string. There is no such type, so it is parsed as never type.

Contextual typing context class

Typescript has some obvious places for type inference.

declare function map<T, U>(f: (t: T) => U, ts: T[]): U[];
let sns = map((n) => n.toString(), [1, 2, 3]);

Here, the same is true for n: number in this example, although T and U have not been inferred before the call. In fact, after using [1,2,3] to infer T=number, the return type of n => n.toString() is used to infer U=string, resulting in sns being of type string[].

declare function run<T>(thunk: (t: T) => void): T;
let i: { inference: string } = run((o) => {
  o.inference = "INSERT STATE HERE";
});

The type of o is determined to be {inference:string} because:

  1. Declares an initializer to be contextually typed according to the declared type: { inference: string }.
  2. The return type of the call is inferred using the context type, so the compiler infers T={ inference: string }.
  3. Arrow functions use context types to type their arguments, so the compiler gives o: { inference: string }.

Type alias

The class name is just an alias and will give the type a new name

  • Basic case
type Size = [number,number]
let x:Size = [122,125]
  • Create new types with & amp;
type FString = string & amp; { __compileTimeOnly: any };

FString is just like a normal string, except that the compiler thinks it has a property called __compileTimeOnly, but it doesn’t actually exist, which means that FString can still be assigned to string, but not vice versa.

The difference between interface and type

Note: Type aliases and interfaces are very similar, and in many cases you can choose between them freely. Almost all functions of interface can be used in type. The main difference is that the type cannot be reopened to add new properties, while interface can always be extended.

  • Category names cannot participate in declaration merging, interfaces can

How to expand the two

  • Interfaces can only be used to declare the type of objects and cannot rename basic types.

Suggestion: In most cases, you can choose according to personal preference. Generally, interface is used. If it is not satisfied, type can be used.

Discriminant Union

The closest equivalent to data is a type union with discriminant properties

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };

A tag or discriminator is just an attribute in each object type, each variant has the same attribute and a different unit type, this is still a normal union type; the leading | is an optional part of the union type syntax. Members of the union can be distinguished through javascript code

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };
 
function area(s: Shape) {
  if (s.kind === "circle") {
    return Math.PI * s.radius * s.radius;
  } else if (s.kind === "square") {
    return s.x * s.x;
  } else {
    return (s.x * s.y) / 2;
  }
}

The return type of the above code area is inferred to be number, which defaults to the complete code. If some results are not involved, the return type can be modified to number|undefined

Type parameters

Function parameters may also need to have declared types. There is no case requirement, but the type is usually a single uppercase letter. Type parameters can also be constrained to a type. Kind of like type constraints.

function firstish<T extends { length: number }>(t1: T, t2: T): T {
  return t1.length > t2.length ? t1 : t2;
}

Because Typescript is structured, it doesn’t require type parameters like nominal systems. Specifically, they are not required to be functionally polymorphic. Type parameters should only be used to propagate type information, such as constraining parameters to be of the same type

//T is not required; because it is only referenced once, it is not used to constrain the return value or other parameter types.
function length<T extends ArrayLike<unknown>>(t: T): number {}
function length(t: ArrayLike<unknown>): number {}

Module system

Import and export via import and export

//Import
import { value, Type } from "npm-package";
import * as prefix from "../lib/third-package";
import f = require("single-function-package"); //Use node.js module through commonjs module

readonly and const

  • Application of const

In Javascript, although it allows variable declarations with const, references can still become immutable when declared.

const a = [1, 2, 3];
a.push(102); // ):
a[0] = 101; // D:

Use const for assertions on objects or arrays.

//Array assertion
let a = [1, 2, 3] as const;
a.push(102); // error
a[0] = 101; // error

//Object assertion (advantage: you can clearly see the attribute content when using it)
const test = {
  name:'124',
  age:13
} as const
  • readonly application
//Use of attributes
interface Rx{
   readoly x:number
}

let rx: Rx = { x: 1 };
rx.x = 12; // error

//Use of interface
interface
  x: number;
}
let rx: Readonly<X> = { x: 1 };
rx.x = 12; // error

//Usage of array
let a: ReadonlyArray<number> = [1, 2, 3];
let b: readonly number[] = [1, 2, 3];
a.push(102); // error
b[0] = 101; // error

Non-null assertion operator (suffix!)

Typescript also has a special syntax that allows you to remove null and undefined from a type without doing an explicit check. When expressing, write ! is actually a type assertion that the value is not null or undefined

function liveDangerously(x?: number | null) {
  //No error
  console.log(x!.toFixed());
}
//When we know that x is not null or undefined, it is very important to use!

Writing standards

  • In some cases, typescript can infer the type for us. It is recommended not to fill in the type manually.

  • Use noImplicitAny to control any
  • Use strictNullChecks to control undefined or null

FAQ

  • Type assertion
declare function handleRequest(url: string, method: "GET" | "POST"): void;
 
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);

//Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.

Solution one:

//Explicit type is GET
handleRequest(req.url, req.method as "GET"); 

Solution 2

//The as const suffix works similarly to the one used in the diagram above, but is used by the type system to ensure that all correspondence is assigned a literal type. instead of a generic version like string or number
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);