在打字稿中如何基于其他两个类型的键类型

时间:2019-07-23 07:32:49

标签: typescript typescript-typings typescript-generics

想象一个函数getValidation,它需要一些状态和相应的模式来验证状态。一个例子:

type State = {
  selectedColor: string
  selectedSize: string
  selectedOptions?: Record<string, string>
}

type StateSchema = {
  selectedColor: {
    required: (val: any) => boolean
  }
  selectedSize: {
    required: (val: any) => boolean
  }
  selectedOptions?: Record<string, { required: (val: any) => boolean }>
}

const state: State = {
  selectedColor: '',
  selectedSize: 'small',
}

const schema: StateSchema  = {
  selectedColor: {
    required: (val: any) => Boolean(val)
  },
  selectedSize: {
    required: (val: any) => Boolean(val)
  }
}

const validation = getValidation(
  schema,
  state
)

// validation
{
  $isValid: false,
  $value: {
    selectedColor: '',
    selectedSize: 'small',
  }
  selectedColor: {
    $isValid: false,
    $value: '',
    $validations: {
      required: false
    }
  },
  selectedSize: {
    $isValid: true,
    $value: 'small',
    $validations: {
      required: true
    }
  },
}

const state2 = {
  selectedColor: '',
  selectedSize: 'small',
  selectedOptions: {
    fit: 'tight',
    length: ''
  }
}

const schema2 = {
  selectedColor: {
    required: (val: any) => Boolean(val)
  },
  selectedSize: {
    required: (val: any) => Boolean(val)
  },
  selectedOptions: {
    fit: {
      required: (val: any) => Boolean(val)
    },
    length: {
      required: (val: any) => Boolean(val)
    }
  }
}

const validation2 = getValidation(
  schema2,
  state2
)

// validation2
{
  $isValid: false,
  $value: {
    selectedColor: '',
    selectedSize: 'small',
    selectedOptions: {
      fit: 'tight',
      length: ''
    }
  }
  selectedColor: {
    $isValid: false,
    $value: '',
    $validations: {
      required: false
    }
  },
  selectedSize: {
    $isValid: true,
    $value: 'small',
    $validations: {
      required: true
    }
  },
  selectedOptions: {
    $isValid: false,
    $value: {
      fit: 'tight',
      length: ''
    },
    fit: {
      $isValid: true,
      $value: 'tight',
      $validations: {
        required: true
      }
    },
    length: {
      $isValid: false,
      $value: '',
      $validations: {
        required: false
      }
    },
  },
}

关于上述示例的注意事项:

  • 状态可以是用户定义的任何对象
  • 模式结构必须与状态结构匹配,直到模式定义一个对象,其中所有键都是用于验证状态中该点的函数。
  • 结果验证结构应与状态结构相匹配,并添加一些附加内容。 $ isValid和$ value将添加到状态对象的每个级别。如果架构定义了验证器对象,则应将相应的验证器密钥添加到$ validations密钥中。

如何为这样的模式编写通用类型或接口,该模式取决于另一种类型的结构(在这种情况下为状态)?

如何根据状态和架构类型的结构,编写从getValidation得到的验证的通用类型或接口?

1 个答案:

答案 0 :(得分:1)

不确定您问题中“用户定义的任何对象”的含义,因为TypeScript类型仅适用于编译时而不适用于运行时,因此,如果用户仅在您需要时在运行时提供这些值使用另一种方法。

我在这个答案中假设用户是使用您的框架的开发人员,或者您将把用户所需的结构编码为TypeScript。

您可以使用以下内容将以下内容放在一起:

请注意,我在这里忽略了数组,我将您的选项从“记录”交换为State类型的普通对象,但是它的工作原理相同。

type SchemaEntry<T> = ObjectSchemaEntry<T> | PrimativeSchemaEntry<T>;

type PrimativeSchemaEntry<T> = {
  [validationName: string]: (val: T) => boolean;
}

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

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

type ValidationResultEntry<T, S> = 
  S extends ObjectSchemaEntry<T> ? ObjectValidationResultEntry<T, S> : 
  S extends PrimativeSchemaEntry<T> ? PrimativeValidationResultEntry<T, S> : 
  never;

type PrimativeValidationResultEntry<T, S extends PrimativeSchemaEntry<T>> = {
  $isValid: boolean;
  $value: T;
  $validations: {
    [P in keyof S]: boolean;
  };
};

type ObjectValidationResultEntry<T, S extends ObjectSchemaEntry<T>> = {
  [P in keyof T]: ValidationResultEntry<T[P], S[P]>;
} & {
  $isValid: boolean;
  $value: T;
};

type ValidationResult<T, S extends Schema<T>> = {
  [P in keyof T]: ValidationResultEntry<T[P], S[P]>;
} & {
  $isValid: boolean;
  $value: T;
};

function inferStateTypeFrom<T>() {
  return <S extends T>(state: S): S => state;
}

function inferSchemaTypeFrom<T>() {
  return <S extends Schema<T>>(schema: S): S => schema;
}

然后您可以像这样使用它...

type State = {
  selectedColor: string
  selectedSize: string
  selectedOptions?: { [key: string]: string }
}

const state = inferStateTypeFrom<State>()({
  selectedColor: '',
  selectedSize: 'small',
  selectedOptions: {
    fit: 'tight',
    length: ''
  }
});

const schema = inferSchemaTypeFrom<typeof state>()({
  selectedColor: {
    required: (val) => Boolean(val)
  },
  selectedSize: {
    required: (val) => Boolean(val)
  },
  selectedOptions: {
    fit: {
      foo: (val) => Boolean(val)
    },
    length: {
      bar: (val) => Boolean(val)
    }
  }
});

const result: ValidationResult<typeof state, typeof schema> = {
  $isValid: false,
  $value: {
    selectedColor: '',
    selectedSize: 'small',
    selectedOptions: {
      fit: '',
      length: ''
    }
  },
  selectedColor: {
    $isValid: false,
    $value: '',
    $validations: {
      required: false
    }
  },
  selectedSize: {
    $isValid: true,
    $value: 'small',
    $validations: {
      required: true
    }
  },
  selectedOptions: {
    $isValid: false,
    $value: {
      fit: '',
      length: ''
    },
    fit: {
      $isValid: true,
      $value: '',
      $validations: {
        foo: true
      }
    },
    length: {
      $isValid: false,
      $value: '',
      $validations: {
        bar: true
      }
    }
  }
};

特殊的调味料在infer*函数中并使用typeof variable。由于State和通用架构中的类型信息不完整,我们需要使用推断出的实际状态和架构对象的类型来使类型检查正常进行。我们想要一个从某种已知类型(即状态和模式)派生的推断类型,这变得很复杂,这就是infer*函数起作用的地方。除了使TypeScript推断类型外,它们实际上没有做任何其他事情,因为我们没有为内部函数提供通用参数。

根据state推断schema的类型,然后根据typeof state推断ValidationResult<typeof state, typeof schema>的类型,然后允许我们将结果类型设置为combineLatest,这使我们全类型安全。

如果将上面的代码放入TypeScript playground中,则可以通过将鼠标悬停在变量名称上来查看推断的类型;如果尝试更改名称和事物类型,则会看到编译器警告。开始输入文字时,您还应该获得自动完成建议。

相关问题