This article will help you understand Typescript with simple examples of common development issues. Because there are many articles on the basics of Typescript and the official documentation itself is comprehensive, the basics of Typescript won’t be covered in this article.

Why use Typescript?

Before we get started, why use Typescript?

Before Typescript, most projects were developed using native Javascript. Javascript is by nature a “flexible” language. Flexibility is the ability to do whatever you want in code, such as summing numbers and arrays, calling methods that don’t exist on objects, passing in functions that don’t meet your expectations, and so on, without any obvious errors.

const number = 1;
const arr = [1.2.3];

console.log(number + arr);

const obj = {};
obj.functionNotExist();

function pow2(value) {
  return Math.pow(value, 2);
}
pow2("bazzzzzz");
Copy the code

In large projects, a “small change” of a type can result in many code tweaks that are hard to spot with the naked eye. Type-safe is the main reason we use Typescript to prevent programs from doing the wrong thing with type declarations.

const number = 1;
const arr = [1.2.3];

console.log(number + arr); The + operator cannot be applied to the types "number" and "number[]".

const obj = {};
obj.noExistFunction(); // There is no property "noExistFunction" on type "{}".

function pow(value: number) {
  return Math.pow(value, 2);
}
pow("bazzzzzz"); // Parameters of type "string" cannot be assigned to parameters of type "number".
Copy the code

Subtle differences

Some keywords in Typescript have subtle conceptual differences that help you write better code.

any vs unknown

Any represents any type, which escapes Typescript type checking. As in Javascript, variables of any type can perform any operation without error at compile time. Unknown can also mean any type, but it also tells Typescript developers that they know nothing about it and need to be careful what they do. This type can perform only a limited operation (= = = = =, | |, &&,? ,! , Typeof, Instanceof, etc.), and other operations need to prove to Typescript what type the value is, otherwise an exception will be raised.

let foo: any
let bar: unknown

foo.functionNotExist()
bar.functionNotExist() // The object is of type "unknown".

if(!!!!! bar) {/ / = = = = =, | |, &&,? ,! , typeof, instanceof
  console.log(bar)
}

bar.toFixed(1) // Error

if (typeof bar=== 'number') {
  bar.toFixed(1) // OK
}
Copy the code

Any increases the risk of runtime errors and should not be used unless absolutely necessary. Unknown is used in scenarios where the type is unknown.

{} vs object vs Object

Object represents a normal Javascript object type, not an underlying data type.

declare function create(o: object) :void;

create({ prop: 0 }); // OK
create(null); // Error
create(undefined); // Error
create(42); // Error
create("string"); // Error
create(false); // Error
create({
  toString() {
    return 3; }});// OK
Copy the code

{} represents any type of non-null, non-undefined.

declare function create(o: {}) :void;

create({ prop: 0 }); // OK
create(null); // Error
create(undefined); // Error
create(42); // OK
create("string"); // OK
create(false); // OK
create({
  toString() {
    return 3; }});// OK
Copy the code

Object and {} are almost identical, except that the Object type validates the built-in methods of the Object prototype (toString/hasOwnPreperty).

declare function create(o: Object): void; create({ prop: 0 }); // OK create(null); // Error create(undefined); // Error create(42); // OK create("string"); // OK create(false); // OK create({ toString() { return 3; }}); // ErrorCopy the code

If you need an object type, but have no requirements for the object’s properties, use Object. {} and Object represent the scope is too general to use.

type vs interface

Both can be used to define types.

Interfaces can only declare object types and support declaration merge (extensible).

interface User {
  id: string
}
 
interface User {
  name: string
}
 
const user = {} as User
 
console.log(user.id);
console.log(user.name);
Copy the code

Type (type alias) does not support declarative merging, behaves a bit like const, and lets have block-level scope.

type User = {
  id: string,}if (true) {
  type User = {
    name: string,}const user = {} as User;
  console.log(user.name);
  console.log(user.id) // Attribute "id" does not exist on type "User".
}
Copy the code

Type is more general, and the right side can be any type, including expression operations, mapping types, and so on.

type A = number
type B = A | string
type ValueOf<T> = T[keyof T];
Copy the code

If you’re developing a package, a module, that allows others to extend it, use interface, if you need to define underlying data types or need type operations, use Type.

enum vs const enum

Enum is compiled into Javascript objects by default and can be looked up backwards by value.

enum ActiveType {
  active = 1,
  inactive = 2,}function isActive(type: ActiveType) {}
isActive(ActiveType.active);

// ============================== compile result:
// var ActiveType;
// (function (ActiveType) {
// ActiveType[ActiveType["active"] = 1] = "active";
// ActiveType[ActiveType["inactive"] = 2] = "inactive";
// })(ActiveType || (ActiveType = {}));
// function isActive(type) { }
// isActive(ActiveType.active);

ActiveType[1]; // OK
ActiveType[10]; // OK!!
Copy the code

Cosnt enum does not generate Javascript objects by default. Instead, it outputs values directly to the code used.

const enum ActiveType {
  active = 1,
  inactive = 2,}function isActive(type: ActiveType) {}
isActive(ActiveType.active);

// ============================== compile result:
// function isActive(type) { }
// isActive(1 /* active */);

ActiveType[1]; // Error
ActiveType[10]; // Error
Copy the code

Enum is an error-prone way to index values in parentheses. Const enum is a safer type than enum.

Script mode and module mode

Typescript has two modes: Script mode, where a file corresponds to an HTML Script tag, and Module mode, where a file corresponds to a Typescript Module. The distinguishing logic is that the file content package does not contain the import or export keyword.

Understanding the difference between these two patterns helps you understand some of the quirks of writing demo code.

In script mode, all variable definitions and type declarations are global. Multiple files defining the same variable will generate an error and the interface with the same name will be merged. In modular mode, all variable definitions and type declarations are valid within the module.

There are also differences between the two modes when writing type declarations. For example, in scripting mode, declare var GlobalStore to write declarations for global objects.

GlobalStore.foo = "foo";
GlobalStore.bar = "bar"; // Error

declare var GlobalStore: {
  foo: string;
};
Copy the code

In modular mode, declaring Global is required to write declarations for global objects

GlobalStore.foo = "foo";
GlobalStore.bar = "bar";

declare global {
  var GlobalStore: {
    foo: string;
    bar: string;
  };
}

export {}; // The export keyword changes the mode of the file
Copy the code

Type of operation

This chapter introduces Typescript’s common type operators.

Set operations

The & represents the bit and operator in JS and is used to evaluate the intersection of two types in Typescript.

type Type1 = "a" | "b";
type Type2 = "b" | "c";
type Type3 = Type1 & Type2; // 'b'
Copy the code

| in JS or operator, said in a Typescript and used to calculate two types of sets.

type Type1 = "a" | "b";
type Type2 = "b" | "c";
type Type3 = Type1 | Type2; // 'a' 'b' 'c'
Copy the code

The index sign

Index signatures can be used to define the types of properties and values within objects. For example, defining a React component allows Props to pass any Props with a key of string and a value of number.

interface Props {
  [key: string] :number
}

<Component count={1} / >// OK
<Component count={true} /> // Error
<Component count={'1'} / > // Error
Copy the code

Type type

Type typing allows Typescript to use types as objects take property values.

type User = {
  userId: string
  friendList: {
    fristName: string
    lastName: string}} []type UserIdType = User['userId'] // string
type FriendList = User['friendList'] // { fristName: string; lastName: string; } []
type Friend = FriendList[number] // { fristName: string; lastName: string; }
Copy the code

In the example above, we used type typing to calculate several other types from the User type. FriendList[number] The number keyword is used to get the type of the subitem of the array. You can also use literal numbers in tuples to get the types of array elements.

type Tuple = [number.string]
type First = Tuple[0] // number
type Second = Tuple[1] // string
Copy the code

typeof value

The typeof keyword is used in JS to get the typeof a variable, resulting in a string (value). In TS, the type of a variable is inferred (type)

let str1 = 'fooooo'
type Type1 = typeof str1 // type string

const str2 = 'fooooo'
type Type2 = typeof str2 // type "fooooo"
Copy the code

Typeof evaluates variables differently than constants. Since constants do not change, Typescript uses strict types, such as str2, which is a string of type ‘fooooo’. The variable will be a loose string type.

keyof Type

The keyof keyword can be used to get all key types of an object type.

type User = {
  id: string;
  name: string;
};

type UserKeys = keyof User; //"id" | "name"
Copy the code

Enum is special in Typescript (sometimes representing both a type and a value). To retrieve enum’s key type, use Typeof and then keyof as a value.

enum ActiveType {
  Active,
  Inactive
}

type KeyOfType = keyof typeof ActiveType // "Active" | "Inactive"
Copy the code

extends

The extends keyword also has several uses. It means type extension in interface, Boolean operations in conditional type statements, restrictions in generics, and inheritance in class.

// indicates type extension
interface A {
  a: string
}

interface B extends A { // { a: string, b: string }
  b: string
}

// The condition type functions as a Boolean operation
type Bar<T> = T extends string ? 'string' : never
type C = Bar<number> // never
type D = Bar<string> // string
type E = Bar<'fooo'> // string

// act as a type constraint
type Foo<T extends object> = T
type F = Foo<number> // Type "number" does not meet constraint "object".
type G = Foo<string> // The type string does not meet the object constraint.
type H = Foo<{}> // OK

/ / class inheritance
class I {}
class J extends I {}
Copy the code

The condition that makes A extends B valid in Boolean operations or generic restrictions is that A is A subset of B, that is, A needs to be more specific than B, or at least as specific as B.

type K = '1' extends '1' | '2' ? 'true' : 'false' // "true"
type L = '1' | '2' extends '1' ? 'true' : 'false' // "false"

type M = { a: 1 } extends { a: 1.b: 1}?'true' : 'false' // "false"
type N = { a: 1.b: 1 } extends { a: 1}?'true' : 'false' // "true"
Copy the code

is

The IS keyword is used in Typescript as user type protection to tell Typescript how to recognize types. For example, in the following example, pet isFish is followed by the isFish method, which tells Typescript that pet is a user-validated type of Fish and can safely be identified as Fish when the method returns true. Returning false indicates that PET is not a Fish, so be careful when using it as a Fish.

interface Fish {
  swim: () = >{}}function isFish(pet: any) :pet is Fish {
  return (pet asFish).swim ! = =undefined;
}

let pet = {} as unknown

if (isFish(pet)) {
  pet.swim() // OK
} else {
  pet.swim() // There is no property "swim" on type "Bird"
}
Copy the code

Other keywords you can use to determine types are typeof, instanceof, in, and so on.

The generic

Generics are an important part of Typescript. Next, we introduce generics with a filter method.

Suppose the filter method passes in an array of numeric types and a method that returns a Boolean value, eventually filtering out the desired result and returning the statement roughly as follows.

declare function filter(
  array: number[],
  fn: (item: unknown) => boolean
) :number[];
Copy the code

After a while, you need to use the Filter method to filter some strings. You can use Typescript’s function-overloading functionality. The internal filter code stays the same, just adding the type definition.

declare function filter(
  array: string[],
  fn: (item: unknown) => boolean
) :string[];
declare function filter(
  array: number[],
  fn: (item: unknown) => boolean
) :number[];
Copy the code

After a while, we needed to filter Boolean [], object[], and other concrete types, but if we still used overloaded methods, we would have a lot of duplicate code. This is where you can consider using generics, Dont Repeat Yourself.

Generics are like “methods” in Typescript “language” that can “pass arguments” to get new types. Common generics used in everyday development include Promise, Array, react.component.etc.

Modify the filter method with generics:

declare function filter<T> (
  array: T[],
  fn: (item: unknown) => boolean
) :T[];
Copy the code

Just add Angle brackets

after the method name to indicate that the method supports a generic parameter. (Here T can be changed to any variable name you like. Most people prefer T, U, V… Start naming), array: T[] means that the first argument passed is an array of generic template type, and :T[] means that the method returns an array of template type. Typescript automatically identifies the type T actually represents based on the type of the passed argument, so you can preserve the type without duplicating the code.

filter([1.2.3].() = > true) // function filter<number>(array: number[], fn: (item: unknown) => boolean): number[]
filter(['1'.'2'.'3'].() = > true) // function filter<string>(array: string[], fn: (item: unknown) => boolean): string[]
Copy the code

Much of the behavior is easy to understand when generics are likened to “methods.” Methods can take arguments, can have multiple arguments, can have default values, and so can generics.

type Foo<T, U = string> = { // Multiple parameters, default value
  foo: Array<T> // Can pass
  bar: U
}

type A = Foo<number> // type A = { foo: number[]; bar: string; }
type B = Foo<number.number> // type B = { foo: number[]; bar: number; }
Copy the code

Remember, generic parameters can also have limitations. In the following example extends limits T to at least an HTMLElement type.

type MyEvent<T extends HTMLElement = HTMLElement> = {
   target: T,
   type: string
}
Copy the code

Typescript comes with some generic tools, one by one and the implementation code is included below.

Mapping type

Key words in

The in keyword represents a type mapping in a type, similar to the way index signatures are written. This example declared in a type of Props, the key of type ‘count’ | ‘id’ types, the value for the number type.

type Props = {
  [key in 'count' | 'id'] :number
}

const props1: Props = { // OK
  count: 1.id: 1
}

const props2: Props = {
  count: '1'.// ERROR
  id: 1
}

const props3: Props = {
  count: 1.id: 1.name: 1 // ERROR
}
Copy the code

Record

Record defines an object type of key type Keys and value type Values.

Example:

enum ErrorCodes {
  Timeout = 10001,
  ServerBusy = 10002,}const ErrorMessageMap: Record<ErrorCodes, string> = {
  [ErrorCodes.Timeout]: 'Timeout, please try again',
  [ErrorCodes.ServerBusy]: 'Server is busy now'
}
Copy the code

Type mapping can also be used for comprehensive checking. For example, Typescript also throws an exception if an Errorcode is missing in the example above.

enum ErrorCodes {
  Timeout = 10001,
  ServerBusy = 10002,
  AuthFailed = 10003
}

// Type "{10001: string; 10002: string; }" attribute "10003" is missing in "Record
      
       ", but is required in type "Record
       
        "
       ,>
      ,>
const ErrorMessageMap: Record<ErrorCodes, string> = { 
  [ErrorCodes.Timeout]: 'Timeout, please try again',
  [ErrorCodes.ServerBusy]: 'Server is busy now'
}
Copy the code

Code implementation:

type Record<K extends keyof any, T> = {
  [P in K]: T;
};
Copy the code

Partial

Partial can make type-defined properties optional.

Example:

typeUser = { id? :string.gender: 'male' | 'female'
}

type PartialUser =  Partial<User>  // { id? : string, gender? : 'male' | 'female'}

function createUser (user: PartialUser = { gender: 'male' }) {}
Copy the code

The User type is required for the gender attribute (the User must have a gender). When the createUser method is designed, gender is given a default value for convenience. You can change the Partial

parameter so that the User does not have to pass gender.

Code implementation:

type Partial<T> = {
  [U inkeyof T]? : T[U]; };Copy the code

Required

Required does the opposite of Partial, making all properties of the object type Required.

Example:

typeUser = { id? :string.gender: 'male' | 'female'
}

type RequiredUser = Required<User> // { readonly id: string, readonly gender: 'male' | 'female'}

function showUserProfile (user: RequiredUser) {
  console.log(user.id) // No need to add!
  console.log(user.gender)
}
Copy the code

Still using the User type, the ID attribute is optional when it is defined (it must be created), and the User ID must already exist when it is displayed. In this case, you can use Required

, All attributes of User must be non-undefined when showUserProfile is called.

Code implementation:

type Required<T> = {
  [U inkeyof T]-? : T[U]; };Copy the code

-? The symbol here means to get rid of the optional symbol, right? .

Readonly

Readonly makes all properties of the object type Readonly.

Example:

type ReadonlyUser = Readonly<User> // { readonly id? : string, readonly gender: 'male' | 'female'}

const user: ReadonlyUser = {
  id: '1'.gender: 'male'
}

user.gender = 'femail' // "gender" cannot be assigned because it is read-only.
Copy the code

Code implementation:

type Readonly<T> = {
  readonly [U in keyof T]: T[U];
};
Copy the code

Pick

Pick is part of the attribute of the Pick type.

Example:

type Location = {
  latitude: number
  longitude: number
  city: string
  address: string
  province: string
  district: string
}

type LatLong = Pick<Location, 'latitude' | 'longitude'> // { latitude: number; longitude: number; }

const region: LatLong = {
  latitude: 22.545001.longitude: 114.011712
}
Copy the code

Have a Location type, and now only need the latitude and longitude data, use a Pick < Location, ‘latitude’ | ‘longitude > create new LatLong type.

Code implementation:

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};
Copy the code

Omit

Omit a combination of Pick and Exclude that will Omit part of the keys in the object type.

Example:

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Omit<Todo, "description">; // { title: string; completed: boolean; }

const todo: TodoPreview = {
  title: "Clean room".completed: false};Copy the code

Code implementation:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Copy the code

Conditions in the

Ternary operator

Typescript type operations also support “ternary operators,” called conditional types. The extends keyword determines whether a condition is true, and returns a type if it is true, and another type if it is not. Conditional types are usually used in conjunction with generics (because there is no need to judge if a fixed type is known).

type IsString<T> = T extends string ? true : false

type A = IsString<number> // false
type B = IsString<string> // true
Copy the code

Type also has the condition of processing and set, the logic of conditional distribution, number | string do condition operation is equivalent to number operation | string operation conditions

type ToArray<T> = T[]
type A = ToArray<number | string> // (string | number)[]

type ToArray2<T> = T extends unknown ? T[] : T[];
type B = ToArray2<number | string>; // string[] | number[]
Copy the code

infer

In addition to displaying declared generics parameters, Typescript also supports dynamic derivation of generics using the Infer keyword. When do we need dynamic derivation? It is often necessary to get a new type by passing in a generic parameter, rather than defining a new generic parameter directly.

For example, now that the two concrete types of ApiResponse are defined, UserResponse and EventResponse, what do YOU need to do if you want the User and Event entity types?

type ApiResponse<T> = {
  code: number
  data: T
};

type UserResponse = ApiResponse<{
  id: string.name: string} >type EventResponse = ApiResponse<{
  id: string.title: string} >Copy the code

Of course you can pull it out and define new types.

type User = {
  id: string.name: string
}

type UserResponse = ApiResponse<User>
Copy the code

But if the type is provided by someone else, it’s hard to deal with. Try infer, with the following code:

type ApiResponseEntity<T> = T extends ApiResponse<infer U> ? U : never;

type User = ApiResponseEntity<UserResponse>; // { id: string; name: string; }
type Event = ApiResponseEntity<EventResponse>; // { id: string; title: string; }
Copy the code

In the example, determine whether the incoming type T is a subset of T extends ApiResponse

. Infer here lets Typescript try to understand what type of ApiResponse T is, generating a new generic parameter U. Return type U if the extends condition is met.

With a good understanding of conditional types and the infer keyword, the conditional generics tools that come with Typescript are easy to understand.

ReturnType

Returntype Retrieves the return value type of the method

Example:

type A = (a: number) = > string
type B = ReturnType<A> // string
Copy the code

Code implementation:

type ReturnType<T> = T extends (
  ...args: any[]
) => infer R ? R : any;
Copy the code

Parameters

Parameters is used to get the parameter type of a method

Example:

type EventListenerParamsType = Parameters<typeof window.addEventListener>;
// [type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | undefined]
Copy the code

Code implementation:

type Parameters<T extends(... args:any) = >any> = T extends(... args: infer P) =>any
  ? P : never;
Copy the code

Exclude

Exclude is used to calculate the type that is in T and not in U

Example:

type A = number | string
type B = string
type C = Exclude<A, B> // number
Copy the code

Code implementation:

type Exclude<T, U> = T extends U ? never : T;
Copy the code

Extract

Extract is used to calculate the types of T that can be assigned to U

Example:

type A = number | string
type B = string
type C = Extract<A, B> // string
Copy the code

Code implementation:

type Extract<T, U> = T extends U ? T : never;
Copy the code

NonNullable

Exclude null and undefined from types

Example:

typeA = { a? :number | null
}
type B = NonNullable(A['a']) // number
Copy the code

Code implementation:

type NonNullable<T> = T extends null | undefined ? never : T;
Copy the code

NPM related

How does Typescript and NPM work together?

How do I publish a Typescript NPM package?

Writing NPM modules in Typescript provides users with declaration files so that the compiler can provide type checking and code hints when writing code. Just do the following steps:

  1. intsconfig.jsonFile, configurationdeclaration: trueThis automatically generates a declaration file every time Typescript is compiled.
  2. inpackage.jsonIn the configurationprepublishOnlyScript compiles Typescript code every time NPM publish is performed;
  3. Adjust thepackage.jsonIn themain,typesThe field points to the final code path
// tsconfig.json
{
  "compilerOptions": {
    "declaration": true // automatically generate the declaration file d.ts}}// package.json
{
  "name": "@scope/awesome-typescript-package"."version": "0.0.1"."main": "dist/index.js"."types": "dist/index.d.ts".// The module types path
  "scripts": {
    "tsc": "tsc -p ./tsconfig.json"."prepublishOnly": "npm run tsc" // Compile the code each time before NPM publish}}Copy the code

After a smooth release, users will be prompted with the correct type when they import our module code.

Extend the tripartite module supplement type declaration

Due to the popularity of the Typescript community, most popular NPM packages have type declaration files, or the @types package DefinitelyTyped provided by the community. However, there will occasionally be packages that lack a type definition, or packages that support plug-in extensions, in which case we need to add supplemental type declarations.

Json to add our own path to compileroptions. types. For example, if all types of files are in the types path, you can configure “types”: [“./types/*.d.ts”].

Then in the declaration file, the function of declaration merge is used to extend the corresponding module.

// Extend global jQuery to add plugins
declare global {
  interface JQuery {
    myPlugin: MyPlugin
  }
}
 
// JSX extends the Image feature so that new Web features can be used without updating the version
import 'react'

declare module 'react' {
  interfaceImgHTMLAttributes<T> { loading? :'lazy2' | 'eager'}}Copy the code

Note that it is the type, not the value, that needs to be extended. For example, in React-Native, attributes or methods are exposed to RN terminals via NativeModules (an Object) module via the native terminal. By default, all of its attributes are of type any.

import { NativeModules } from 'react-native'
NativeModules.MApplication.version // Default is any
Copy the code

To expand the types of NativeModules, check the source code of the React-native type to find its type, NativeModulesStatic. You then add the type declaration to the declaration file.

import { NativeModules } from 'react-native'
 
declare module 'react-native' {
  interface NativeModulesStatic {
    MApplication: {
     version: string}}}Copy the code

With the declaration file, Typescript recognizes types correctly.

import { NativeModules } from 'react-native'
NativeModules.MApplication.version // string
Copy the code

WTFFFFFF?

Some strange behavior occurs while working with Typescript. Here’s a list of common problems and an attempt to explain why.

Why can enum/class be used as type as well as value?

Do you get confused when you use enum/class and why they can be used as both types and values? Can developers do the same themselves?

const enum ActiveType {
  active = 1,
  inactive = 2,}function isActive(type: ActiveType) {} / / type
isActive(ActiveType.active); / / value
Copy the code

In fact, Typescript supports a companion object-like pattern that binds types to object pairs. Consumers can import both together.

type Currency = {
  unit: "EUR" | "GBP" | "JPY";
  value: number;
};

const Currency = {
  from: (value: number.unit: Currency["unit"] = "EUR") :Currency= > {
    return {
      unit,
      value: value, }; }};const currency: Currency = Currency.from(10);
Copy the code

Implements an object similar to an enum effect

// enum-like.ts
const ActiveTypes = {
  active: "active" as const.inactive: "inactive" as const};type ValueOf<T> = T[keyof T];
type ActiveTypes = ValueOf<typeof ActiveTypes>;

export { ActiveTypes };

// index.ts
import { ActiveTypes } from "./enum-like";

function isActive(value: ActiveTypes) {
  console.log(value);
}

isActive(ActiveTypes.active);
isActive('zzzzzzzz'); // Error
Copy the code

Why do literals report errors, but not literals after assignment

Suppose you now define a request method that supports passing an Options parameter.

type Options = {
  url: string; method? :"get" | "post";
};

declare function request(options: Options) :void;

request({
  url: "https://juejin.com/api/user".foo: "foo".// Error
});

const options1 = {
  url: "https://juejin.com/api/user".foo: "foo"}; request(options1);// OK
Copy the code

In contrast, calling the Request method and passing arguments directly as object literals raises an exception, while assigning an object to a temporary variable and passing the variable to the Request method clears the exception. {url: string}} {url: string}} {url: string}} {url: string}} {url: string}} {url: string}}

The reason for this is that Typescript turns on redundant property checking for object literals. The advantage of redundant property checking is that it prevents developers from mistyping parameter names.

For example, the developer incorrectly spells method as mothed, which is a bit difficult to check if there is no Typescript error.

request({
  url: "https://juejin.com/api/user".mothed: "post"
});
Copy the code

If intermediate variable passing is really needed, the type can be given at variable declaration time, and redundant attribute checking can also take effect.

const options2: Options = {
  url: "https://juejin.com/api/user".foo: "foo".// Error
};
Copy the code

Why cannot be assigned to the ‘right’ “Boolean | |” right “|” left “undefined” type

Another example of common literal passing causing type exceptions is the ‘fixed’ field when using the ANTD Table component.

const columns = [
  {
    title: 'Available'.dataIndex: 'available'.fixed: 'right'.render: (value: boolean) = > {
      return value ? 'Y' : 'N'}},]return (
  <Table
    columns={columns}// Can not type"string"Assigned to type"boolean|"right"|"left"|undefined"dataSource={dataSource}
    rowKey="id"
  />
}
Copy the code

Found after the passing of the columns, we find the string ‘right’ can’t give “Boolean |” right “|” left “|” undefined type assignment. The reason is that Typescript does “type widening” when it does type derivation, deliberately deriving a broader type. The fix is to tell Typescript that ‘right’ is a constant and not to pass it as a string.

// Add an as const type assertion
const columns = [
  {
    title: 'Available'.dataIndex: 'available'.fixed: 'right' as const.// Right is a constant
    render: (value: boolean) = > {
      return value ? 'Y' : 'N'}},]// Add a type declaration to the columns variable to avoid Typescript inferences
const columns: ColumnsType<Data>  = [
  {
    title: 'Available'.dataIndex: 'available'.fixed: 'right'
    render: (value: boolean) = > {
      return value ? 'Y' : 'N'}},]// Assign the value directly to the component without intermediate processing
<Table
  columns={[
    {
      title: 'Available'.dataIndex: 'available'.fixed: 'right'
      render: (value: boolean) = > {
        return value ? 'Y' : 'N'
      },
    },
  ]}
  dataSource={dataSource}
  rowKey="id"
/>
Copy the code

Why doesn’t Typescript recognize union types

Suppose you have a user event handler that supports two types of events: UserInputEvent Value is string, UserMouseEvent is [number, number], If the event. Value type is string, the target should be HTMLInputElement. Find that Typescript doesn’t fail to distinguish between target types.

type UserInputEvent = {
  value: string;
  target: HTMLInputElement;
};

type UserMouseEvent = {
  value: [number.number];
  target: HTMLElement;
};

type UserEvent = UserInputEvent | UserMouseEvent;

function handle(event: UserEvent) {
  if (typeof event.value === "string") {
    event.value; // string
    event.target; // HTMLInputElement | UserMouseEvent}}Copy the code

The reason is that A | B in the understanding of the Typescript is not only A or B, it is also possible that A, B type. In the example below for Cat | Dog types of objects at the same time gives a Cat attribute, and the properties of a Dog.

type Cat = {
  name: string;
  purrs: boolean;
};

type Dog = {
  name: string;
  barks: boolean;
};

// A member of a union type can also belong to each member
type CatOrDogOrBoth = Cat | Dog;

const a: CatOrDogOrBoth = {  // OK
  name: "foo".purrs: false.barks: false};Copy the code

To distinguish types correctly, literals (strings, numbers, booleans, and so on) need to be marked to tell Typescript that types are mutually exclusive.

type UserInputEvent = {
  type: "UserInputEvent";
  value: string;
  target: HTMLInputElement;
};

type UserMouseEvent = {
  type: "UserMouseEvent";
  value: [number.number];
  target: HTMLElement;
};

type UserEvent = UserInputEvent | UserMouseEvent;

function handle(event: UserEvent) {
  // Union types need to be inferred more explicitly
  if (typeof event.value === "string") {
    event.value; // string
    event.target; // HTMLInputElement | UserMouseEvent
  }

  if (event.type === "UserMouseEvent") {
    event.value; // string
    event.target; // HTMLInputElement}}Copy the code

Why is setTimeout still an error

Assuming that there is now a User type, id for undefined | string, in logUserInfo. Determine the User id, however, continues to carry out the inside setTimeout was Typescript denial, The id may not exist.

typeUser = { id? :string;
};

function logUserInfo(user: User) {
  if(! user.id) {return;
  }

  setTimeout(() = > {
    log(user.id); // Cannot assign type "undefined" to type "string"
  });
}

function log(id: string) {
  console.log(id);
}
Copy the code

The reason is that in the JS event loop, setTimeout, methods are executed in macroTask, and the previous judgment! User. id is not in a call stack, and Typescript cannot be sure that the id of the user reference type will not be changed when the method is executed. So we ignore if (! User.id).

The solution could be plus! Assert that id will not be undefined

setTimeout(() = >{ log(user.id!) ; });Copy the code

However, this approach may lead to more assertions (e.g. User.id)

type UserWithoutId = {};

type UserWithId = {
  id: string;
};

type User = UserWithoutId | UserWithId;

function logUserInfo(user: User) {
  if(! ("id" in user)) {
    return;
  }

  setTimeout(() = > {
    log(user.id);
  });
}

function log(id: string) {
  console.log(id);
}
Copy the code

Why does Typescript still report an AS type assertion?

Typescript often uses AS for type assertions (usually as any), which leads developers to think that AS can do anything and that types can be asserted at will. No, for example, there are two types: Cat(Name, PURrs) and RobotCat(Name, Serial). We want to reuse the sayCatName method (which requires passing in a Cat type) and make a type assertion to Doraemon as Cat before calling the method.

Typescript throws an exception and does not allow such conversions!

type Cat = {
  name: string;
  purrs: boolean;
};

type RobotCat = {
  name: string;
  id: string;
}

function sayCatName (cat: Cat) {
  console.log(cat.name)
}

const doraemon: RobotCat = {
  id: '10000'.name: 'Doraemon'
}

sayCatName(doraemon as Cat) The attribute "purrs" is missing from type "RobotCat", but is required from type "Cat"
Copy the code

The reason is that type assertions can only be used if a type is a subtype of another type. Since any is of the same type as any essential oil, any as Cat, Cat as any are allowed. Cat and RobotCat don’t overlap exactly, and Typescript considers this type assertion unsafe.

In cases where it is clear that the code will not exception, you can make two type assertions to avoid reporting an error.

sayCatName(doraemon as unknown as Cat)
Copy the code

A better way to tweak the method type constraint is to use the Pick type to generate the parent type of Cat so that Typescript only validates the property name.

function sayCatName (cat: Pick<Cat, 'name'>) {
  console.log(cat.name)
}

sayCatName(doraemon)
Copy the code

conclusion

In the process of relearning Typescript, I made up for some of the things I didn’t notice before, realized some misunderstandings in understanding, and gained a better understanding of some Typescript “weird behavior”. Hopefully this article will get you one step closer to moving from Partial

to Typescript.

Due to the sheer volume of Typescript content and the author’s own reasons, there may be defects or omissions in Typescript articles. Please refer to the official Typescript documentation for a complete learning of Typescript.

I also want to thank the Typescript programming book for learning so much.

Recommended reading

  • Typescript online exercises typescript-exercises.github. IO /
  • TypeScript programming item.jd.com/12685323.ht…
  • Malcolmkee.com/today-i-lea…