Once you’re stuck in Typescript, you can’t live without it. Security, smart tips, code refactoring..

But there are a lot of old projects that still use JavaScript? It’s going to be hard to migrate

With Typescript you still end up translating into JavaScript code. I don’t want to use a whole set of build tools. I like to write and run, debug directly, and post directly to NPM.

.


Modern VSCode type inference for JavaScript (underlying Typescript) is pretty awesome:







But it’s not enough, it’s not enough, it’s not enough, it’s not enough, it’s not enough, it’s not enough, it’s not enough, it’s not enough.

String can’t be assigned to number. We wish there was a hint, but there isn’t

💡 the article editor is based on VSCode

💡 This article requires some Typescript basics

💡 recommended to view the original, better typesetting





Turn on type checking for JavaScript

First, make sure your VScode has JavaScript validation enabled:







Second, add at the top of the file// @ts-check

Bingo! VSCode already has a type error





Step 3: Type checker

If you want to programmatically verify that your code has type errors, install Typescript CLI:

$ yarn add typescript -D
Copy the code

Json file, similar to tsconfig.json, with similar configuration parameters (see here), except that JsConfig is Javascript focused. Our configuration is as follows:

{
  "compilerOptions": {
    "target": "esnext"."noEmit": true.// 🔴 turns off the output because we are only doing type checking
    "skipLibCheck": true.// "checkJs": true, // enable type check for all JS files without @ts-check
    "strict": true.// 🔴 Strict type checking
    "moduleResolution": "node".// 🔴 find modules in Node mode
    "jsx": "react".// 🔴 Enable JSX
    "module": "esnext"."rootDir": "src".// 🔴 Source file directory
    "resolveJsonModule": true
  },
  "exclude": ["node_modules"."tests/**/*"]}Copy the code





💡 you can also use tsconfig.json directly. You can use allowJs in tsconfig.json to allow JavaScript processing, and you can turn on JavaScript type checking by checkJs globally (equivalent to adding @ts-check to all JavaScript files). If you want to progressively expand type checking and migration to JavaScript, use @ts-check





Next add the run script to package.json so we can execute:

{
  "scripts": {
    "type-check": "tsc -p jsconfig.json"."type-check:watch": "tsc -p jsconfig.json --watch"}},Copy the code


Run it!

$ yarn type-check
tsc -p jsconfig.json
src/index.js:12:1 - error TS2322: Type '"str"' is not assignable to type 'number'.

12 ins.foo = 'str';
   ~~~~~~~

Found 1 error.

error Command failed with exit code 1.

Copy the code





Progressive type declarations

Relying solely on type inference is not enough. Typescript is not smart enough, and in many cases we need to explicitly tell Typescript what type the entity is.

In JavaScript, type declarations can be made via [JSDoc annotations](https://jsdoc.app/) or.d.ts.

Here’s an attempt to migrate the Typescript conventions of type declarations to JavaScript.





1. The variable

1 Notes on one variable

It’s easy to put type annotations in @type {X}. Type annotations have the same syntax as Typescript:

const str = 'string' // Automatic inference
let count: number
const member: string[] = []
const users: ArrayThe < {id: string.name: string, avatar? :string} > = []let maybe: string | undefined
Copy the code
const str = 'string'; // Automatic inference
/ * *@type {number} * /
let count;
/ * *@type Number ❤️ parentheses can be omitted to save more conciseness */
let count;
/ * *@type {string[]} * /
const member = [];
/ * *@type {Array<{ id: string, name: string, avatar? : string }>} * /
const users = [];
/ * *@type string | undefined */
let maybe
Copy the code





2 Type assertion

var numberOrString: number | string = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = numberOrString as number
Copy the code
/ * * *@type {number | string}* /
var numberOrString = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = / * *@type {number} * / (numberOrString); // Note that parentheses are required
Copy the code





Type definition

Some types can be used in more than one place, and we usually define a type by interface or type and reuse it

1 ️ ⃣ conventional interface

interface User {
  id: string;
  name: string;
  age: number; avatar? :string;
}
Copy the code
/** * JSDoc annotations are usually single-line *@typedef {{ id: string name: string age: number avatar? : string }} User User */

/ * *@type {User} * /
var a

// 🔴 but multiple lines can also be recognized
/ * * *@typedef {{ * id: string * name: string * age: number * avatar? : string * }} User
 */
Copy the code


There is another, more ‘JSDoc’ type definition which has the advantage that you can specify each field with text, useful for editor intelligence hints or document generation:

/ * * *@typedef {Object} User
  * @property {string} * id primary key@property {string} Name the name *@property {number} The age age *@property {string} [avatar] * /
Copy the code





2 ️ ⃣ generics

/ / generics
interfaceCommonFormProps<T> { value? : T; onChange(value? : T):void;
}

// Multiple generics
interface Component<Props, State> {
  props: Props;
  state: State;
}
Copy the code
/ / generics
/ * * *@template T
 * @typedef {{ * value? : T * onChange(value? : T): void * }} CommonFormProps
 */

 / * *@type {CommonFormProps<string>} * /
 var b

// Multiple generics
/ * * *@template Props, State
 * @typedef {{ * props: Props; * state: State; *}} Component
 */

/ * *@type {Component<number, string>} * /
var d
Copy the code





3 Existing types of ️ composition

interface MyProps extendsCommonFormProps<string> { title? :string
}
Copy the code
/ * * *@typedef {{title? : string} & CommonFormProps<string>} MyProps
 */

/ * *@type {MyProps}* /
var e
Copy the code





4️ nickname of type

type Predicate = (data: string, index? :number) = > boolean;
type Maybe<T> = T | null | undefined;
Copy the code
// Type alias
/ * * *@typedef {(data: string, index? : number) => boolean} Predicate* /

 / * *@type {Predicate} * /
 var f

/ * * *@template T
 * @typedef {(T | null | undefined)} Maybe* /

/ * *@type {Maybe<string>} * /
var g
Copy the code


💡 Without generic variables, multiple @typedefs can be written in a single comment:

/ * * *@typedef {(data: string, index? : number) => boolean} Predicate
 * @typedef {{ * id: string * name: string * age: number * avatar? : string * }} User
 */
// There are generic variables that need to stand alone
/ * * *@template T
 * @typedef {(T | null | undefined)} Maybe* /
Copy the code





As mentioned above, except for the @typedef and @template syntax, the rest is basically Typescript.


🙋🏻♂️ then the question is!

  • How do I share types with other files?
  • You think it’s a little wordy
  • The code formatting tool does not support formatting comments
  • .





5️ announcement document


In fact, we can import a type definition from another module by importing:

// @file user.js
/ * * *@typedef {{ * id: string * name: string * age: number * avatar? : string * }} User
 */
Copy the code
// @file index.js
/ * *@type {import('./user').User} * /
Copy the code


A better approach is to create a separate *.d.ts (Typescript pure type declaration file) to store type declarations, such as types.d.ts, and define and export types:

// @file types.d.ts

export interface User {
  id: string;
  name: string;
  age: number; avatar? :string;
}

export interfaceCommonFormProps<T> { value? : T; onChange(value? : T):void;
}

export interface Component<Props, State> {
  props: Props;
  state: State;
}

export interface MyProps extendsCommonFormProps<string> { title? :string
}

export type Predicate = (data: string, index? :number) = > boolean;
export type Maybe<T> = T | null | undefined;
Copy the code
/ * *@type {import('./types').User} * /
const a = {};

/ * *@type {import('./types').CommonFormProps<string>} * /
var b;

/ * *@type {import('./types').Component<number, string>} * /
var e;

/ * *@type {import('./types').MyProps}* /
var f;

/ * *@type {import('./types').Predicate} * /
var g;

/ * *@type {import('./types').Maybe<string>} * /
var h;
Copy the code


💡 If a type is referenced more than once and repeating the import is verbose, you can import them at the top of the file once:

/ * * *@typedef {import('./types').User} User
 * @typedef {import('./types').Predicate} Predicate* /
/ * *@type {User} * /
var a;
/ * *@type {Predicate} * /
var g;
Copy the code





6️ third party statement document

Yes, import(‘module’).type can also import third-party library types. These libraries require type declarations, some of which come with the NPM package (e.g. Vue), while others require you to download the corresponding @types/* declarations (e.g. React).

Use React as an example to install @types/ React:

/ * * *@typedef {{ * style: import('react').CSSProperties * }} MyProps
 */

// Since @types/* is exposed globally by default, such as @types/react exposes the react namespace, it is also ok to write:
/ * * *@typedef {{ * style: React.CSSProperties * }} MyProps
 */
Copy the code





7️ global announcement document

In addition, we can declare types globally and use these type definitions everywhere in the project. As usual, we create a global.d.ts in the project root directory (where jsconfig.json is located)

// @file global.d.ts
/** * Global type definition */
interface GlobalType {
  foo: string;
  bar: number;
}

/** * extends an existing global object */
interface Window {
  __TESTS__: boolean;
  // To expose jquery to window, install @types/jquery
  $: JQueryStatic;
}
Copy the code
/ * *@type {GlobalType} * /
var hh                       / / ✅
window.__TESTS__             / / ✅
const $elm = window$('#id') / / ✅
Copy the code





3. The function

Let’s see how to declare a function as a type:

1️ optional parameter

function buildName(firstName: string, lastName? :string) {
  if (lastName) return firstName + ' ' + lastName;
  else return firstName;
}

function delay(time = 1000) :Promise<void> {
  return new Promise((res) = > setTimeout(res, time));
}
Copy the code
// 🔴 JSDoc comment style
/ * * *@param {string} FirstName name *@param {string} [lastName] lastName with square brackets for optional JSDoc arguments *@returns String is optional, Typescript can infer this, and if it cannot, it displays the declaration */
function buildName(firstName, lastName) {
  if (lastName) return firstName + ' ' + lastName;
  else return firstName;
}
buildName('ivan') / / ✅

/ * * *@param {number} [time=1000] Delay time, expressed in ms *@returns {Promise<void>}* /
function delay(time = 1000) {
  return new Promise((res) = > setTimeout(res, time));
}
Copy the code
// 🔴 you can also use Typescript style, ** but not recommended! ** It has the following problems:
// - Cannot add parameter comments, or the tool does not recognize them
// - The handling of optional arguments is a bit inconsistent with Typescript behavior
/ * *@type {(firstName: string, lastName? : string) => string} * /
function buildName(firstName, lastName) {
  if (lastName) return firstName + ' ' + lastName;
  else return firstName;
}

/ / ❌ because TS buildName statement to (firstName: string, lastName: string | undefined) = > string
buildName('1') 
Copy the code
// 🔴 Another way to declare optional arguments is to explicitly set default values (ES6 standard) for optional arguments, which TS will infer to be optional

/ * * *@param {string} FirstName name *@param {string} [lastName= "] lastName with square brackets for optional JSDoc arguments *@returns String is optional, Typescript can infer this, and if it cannot, it displays the declaration */
function buildName(firstName, lastName = ' ' ) {
  if (lastName) return firstName + ' ' + lastName;
  else return firstName;
}

/ * *@type {(firstName: string, lastName? : string) => string} * /
function buildName(firstName, lastName = ' ') {
  if (lastName) return firstName + ' ' + lastName;
  else return firstName;
}
buildName('1') / / ✅

Copy the code





2️ residual parameter

function sum(. args:number[]) :number {
  return args.reduce((p, c) = > p + c, 0);
}
Copy the code
/ * * *@param  {... number} args 
 */
function sum(. args) {
  return args.reduce((p, c) = > p + c, 0);
}
Copy the code





3️ generalization and generalization constraint

function getProperty<T.K extends keyof T> (obj: T, key: K) {
  return obj[key];
}
Copy the code
/ * * *@template T
 * @template {keyof T} K // can constrain the type * of a generic variable in {}@param {T} obj
 * @param {K} key* /
function getProperty(obj, key) {
  return obj[key];
}
Copy the code





4️ parameter statement

interface User {
  name: string;
  lastName: string;
}
const user = {
  name: 'ivan'.lastName: 'lee'.say(this: User) {
    return `Hi, I'm The ${this.name} The ${this.lastName}`; }};Copy the code
/ * * *@typedef {{ * name: string; * lastName: string; *}} User
 */
const user = {
  name: 'ivan'.lastName: 'lee'./ * *@this {User} * /
  say() {
    return `Hi, I'm The ${this.name} The ${this.lastName}`; }};Copy the code

This basically covers the basic usage scenarios of JavaScript functions, and the rest goes on and on.





Class 4.

1️ conventional usage

class Animal {
  private name: string;
  constructor(theName: string) {
    this.name = theName;
  }
  move(distanceInMeters: number = 0) {
    console.log(`The ${this.name} moved ${distanceInMeters}m.`); }}Copy the code

You can declare the accessibility of fields or methods using @public, @private, or @protected.

🔴 if the target environment supports ES6 class
class Animal {
  / * * *@param {string} theName* /
  constructor(theName) {
    // Attribute declaration
    / * * *@type {string}
     * @private Declared private, and the corresponding@protected, the default is@public* You can also use ES6's private Field language feature */
    this.name = theName;
  }
  / * * *@param {number} [distanceInMeters]
   */
  move(distanceInMeters = 0) {
    console.log(`The ${this.name} moved ${distanceInMeters}m.`); }}const a = new Animal('foo');
a.name; // ❌ cannot access private fields
Copy the code
// 🔴 If the target environment does not support it, you can only use functional form
/ * * *@constructor
 * @param {string} theName* /
function Animal1(theName) {
  / * * *@type {string} The private directive */ cannot be used here
  this.name = theName;
}

/ * * *@param {number} [distanceInMeters = 0]
 */
Animal1.prototype.move = function (distanceInMeters) {
  console.log(this.name + ' moved ' + (distanceInMeters || 0) + 'm.');
};

const a = Animal1('bird') // ❌ can only be called new with @constructor

Copy the code





2 ️ ⃣ generics

import React from 'react';

 
export interface ListProps<T> {
  datasource: T[]; idKey? :string;
}

export default class List<T> extends React.Component<ListProps<T>> {
  render() {
    const { idKey = 'id', datasource } = this.props;
    return datasource.map(
      (i) = > React.createElement('div', { key: i[idKey] }) / *... * /,); }}Copy the code
// @ts-check
import React from 'react';

/ * * *@template T
 * @typedef {{ * datasource: T[]; * idKey? : string; *}} ListProps
 */

/ * * *@template T
 * @extends {React.Component<ListProps<T>>} Extends type */ using the extends declaration
export default class List extends React.Component {
  render() {
    const { idKey = 'id', datasource } = this.props;
    return datasource.map(
      // @ts-expect-error Comments such as @ts-ignore can also be used in JavaScript
      (i) = > React.createElement('div', { key: i[idKey] }) / *... * /,); }}// Explicitly define the type
/ * *@type {List<{id: string}>} * /
var list1;
// Automatically infer the type
var list2 = <List datasource={[{ id: 1}}] ></List>;
Copy the code

⚠️ does not support generic variable defaults





3️ interface realization

interface Writable {
  write(data: string) :void
}

class Stdout implements Writable {
  // @ts-expect-error ❌
  write(data: number){}}Copy the code
/ * * *@typedef {{ * write(data: string): void * }} Writable
 */

/ * * *@implements {Writable}* /
class Output {
  / * * *@param {number} data 
   */
  write(data) { // ❌ data should be string}}Copy the code





6. Other

The enumeration

@enum can only be used to constrain the member type of an object

/ * *@enum {number} Mode*/
const Mode = {
  Dark: 1.Light: 2.Mixed: 'mix' / / ❌
};

/ * * *@type {Mode}* /
var mode = 3; // ✅ is not very useful as a type
Copy the code





@deprecated

class Foo {
  / * *@deprecated * /
  bar(){}},new Foo).bar ❌ errors are reported in Typescript 4.0
Copy the code





conclusion

“Typescript-free programming” is a bit of a title party, but instead of using Typescript, we’re using Typescript in a non-intrusive way.

JSDoc annotations are used to declare types in a way that meets most of JavaScript’s type-checking requirements, but many Typescript features don’t.

This way, JSDoc annotations can also be used for document generation by taking advantage of the benefits of Typescript and developing the habit of writing your own comments. Why not





This article refers to the following JSDoc annotations:

  • @type declares entity types, such as variables and class members
  • @typedef Defines a type
  • @template declares generic variables
  • @param defines function parameter types
  • Returns Defines the return value type of a function
  • @constructor or @class declare a function to be a constructor
  • @extends declares class inheritance
  • @implements declares which interfaces the class implements
  • @public, @private, and @protected declare the accessibility of class members
  • @enum Indicates the member type of the constraint object
  • @deprecated Deprecates an entity
  • The @property and @typedef combine to declare the type of a field


Annotations other than the JSDoc annotations mentioned in this article and the Typescript official documentation are not currently supported.





Next, take a look at the Typescript documentation to explore JSDoc and the following use cases (welcome to add) :

  • Typescript JSDoc Playground
  • babel-plugin-trim-helpers

If you discover more, let me know in the comments.





extension

  • Typescript: Type Checking JavaScript Files
  • Typescript: JSDoc Reference
  • Typescript: TSConfig Reference
  • VSCode: Working with JavaScript
  • VSCode: jsconfig.json
  • JSDoc