Use a Decorator to restrict types

A Decorator can be used to restrict the return type of a class method as follows:

const TestDecorator = (a)= > {
  return (
    target: Object,
    key: string | symbol,
    descriptor: TypedPropertyDescriptor<(a)= > number>   // The return value of the function must be number) = > {// Other code}}class Test {
  @TestDecorator()
  testMethod() {
    return '123';   // Error: Type 'string' is not assignable to type 'number'}}Copy the code

You can also use generics to make the input parameter types of TestDecorator compatible with the return parameter types of testMethod:

const TestDecorator = <T>(para: T) = > {
  return (
    target: Object,
    key: string | symbol,
    descriptor: TypedPropertyDescriptor<(a)= > T>
  ) => {
    // Other code}}class Test {
  @TestDecorator('hello')
  testMethod() {
    return 123;      // Error: Type 'number' is not assignable to type 'string'}}Copy the code

Type inference for generics

After a generic type is defined, there are two ways to use it, either by passing in the generic type or by using type inference, where the compiler deduces the generic type from other parameter types. A simple example is as follows:

declare function fn<T> (arg: T) :T;      // Define a generic function

const fn1 = fn<string> ('hello');        // The first way is to pass in the generic type string
const fn2 = fn(1);                      // The second way is to infer that the type of the generic T is number from the type passed in as arg
Copy the code

It is often used with mapping types to implement more complex functions.

Vue Type simple implementation

Here’s an example:

type Options<T> = {
  [P in keyof T]: T[P];
}

declare function test<T> (o: Options<T>) :T;

test({ name: 'Hello' }).name     // string
Copy the code

The test function takes all of the attributes of the passed argument, and now we’re going to do it step by step to achieve the desired function.

First, change the form of the passed argument from {name: ‘Hello’} to {data: {name: ‘Hello’}}. The return type of the called function remains the same, i.e. Test ({data: {name: ‘Hello’}}).name is also a string.

This is not complicated, it just needs to set the data type of the passed parameter to T:

declare function test<T> (o: { data: Options<T> }) :T;

test({data: { name: 'Hello' }}).name     // string
Copy the code

It also works when a data object contains a function:

const param = {
  data: {
    name: 'Hello',
    someMethod() {
      return 'hello world'
    }
  }
}

test(param).someMethod()    // string
Copy the code

Next, consider a special function scenario, like Computed in Vue, that can extract the return value type of a function without calling it. Now the form of the passed parameter is changed to:

const param = {
  data: {
    name: 'Hello'
  },
  computed: {
    age() {
      return 20; }}}Copy the code

The type of a function can simply be thought of as the form () => T, and the type of a method on an object can be thought of as a: () => T, which you can use in reverse derivation (the function returns a value to infer the type of type A). Now, you need to add a mapping type Computed

to handle functions in Computed:

type Options<T> = {
  [P in keyof T]: T[P]
}

type Computed<T> = {
  [P in keyof T]: (a)= > T[P]
}

interface Params<T, M> {
  data: Options<T>;
  computed: Computed<M>;
}

declare function test<T.M> (o: Params<T, M>) :T & M;

const param = {
  data: {
    name: 'Hello'
  },
  computed: {
    age() {
      return 20
    }
  }
}

test(param).name    // string
test(param).age     // number
Copy the code

Finally, using the ThisType mapping type mentioned in TypeScript 1, it is easy to access data in a computed age method:

type Options<T> = {
  [P in keyof T]: T[P]
}

type Computed<T> = {
  [P in keyof T]: (a)= > T[P]
}

interface Params<T, M> {
  data: Options<T>;
  computed: Computed<M>;
}

declare function test<T.M> (o: Params<T, M> & ThisType<T & M>) :T & M;

test({
  data: {
    name: 'Hello'
  },
  computed: {
    age() {
      this.name;    // string
      return 20; }}})Copy the code

So far, only data, computed simple version of Vue Type has been implemented.

A flat array builds a tree structure

Parent_id (or any other) into a tree structure:

// Data before conversion
const arr = [
  { id: 1, parentId: 0, name: 'test1'},
  { id: 2, parentId: 1, name: 'test2'},
  { id: 3, parentId: 0, name: 'test3'}];/ / after the transformation
[
  {
    id: 1,
    parentId: 0,
    name: 'test1',
    children: [
      { id: 2, parentId: 1, name: 'test2', children: [] }
    ]
  },
  {
    id: 3,
    parentId: 0,
    name: 'test3',
    children: []
  }
]
Copy the code

If the children field name does not change, the type of the function is not difficult to write. It would look something like this:

interface Item {
  id: number;
  parentId: number;
  name: string;
}

type TreeItem = Item & { children: TreeItem[] | [] };

declare function listToTree(list: Item[]) :TreeItem[];

listToTree(arr).forEach(i= > i.children)    // ok
Copy the code

But in many cases, the name of the children field is not fixed, but passed in from the argument:

const options = {
  childrenKey: 'childrenList'
}

listToTree(arr, options);
Copy the code

At this point, the children field name should be childrenList:

[
  {
    id: 1,
    parentId: 0,
    name: 'test1',
    childrenList: [
      { id: 2, parentId: 1, name: 'test2', childrenList: [] }
    ]
  },
  {
    id: 3,
    parentId: 0,
    name: 'test3',
    childrenList: []
  }
]
Copy the code

The options parameter is passed in to get the childrenKey type and pass it to the TreeItem.

interface Options<T extends string> {   // The limit is string
  childrenKey: T;
}

declare function listToTree<T extends string = 'children'> (list: Item[], options: Options<T>) :TreeItem<T> [];
Copy the code

T is correctly derived to childrenList when options is {childrenKey: ‘childrenList’}. Next, just change the children to T in the TreeItem:

interface Item {
  id: number;
  parentId: number;
  name: string;
}

interface Options<T extends string> {
  childrenKey: T;
}

type TreeItem<T extends string> = Item & { [key in T]: TreeItem<T>[] | [] };

declare function listToTree<T extends string = 'children'> (list: Item[], options: Options<T>) :TreeItem<T> [];

listToTree(arr, { childrenKey: 'childrenList' }).forEach(i= > i.childrenList)    // ok
Copy the code

There are a few limitations. Due to the effect of Fresh on object literals, when options are not passed in as object literals, you need to give it an assertion:

const options = {
  childrenKey: 'childrenList' as 'childrenList'
}

listToTree(arr, options).forEach(i= > i.childrenList)    // ok
Copy the code

More and more

  • Working with TypeScript (3)
  • Working with TypeScript (2)
  • Working with TypeScript
  • Understand TypeScript in depth