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

In typescript how to base a type of two other types keys

想象一个函数 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
      }
    },
  },
}

以上例子需要注意的地方:

如何为这种依赖于另一种类型(在本例中为状态)的结构的模式编写泛型类型或接口?

您将如何编写 getValidation 生成的验证的通用类型或接口,这取决于状态和模式类型的结构?

不确定问题中 "any object a user defines" 的确切含义,因为 TypeScript 类型仅适用于编译时而不适用于运行时,因此如果用户仅在运行时提供这些值,则您需要使用另一种方法。

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

您可以使用以下内容组合起来:

请注意,我在这里忽略了数组,我将您的选项从 Record 交换为 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 和通用模式内容的类型信息不完整,我们需要使用实际状态和模式对象的推断类型来使类型检查正常工作。由于我们想要从某些已知类型(即 State 和 Schema)派生的推断类型,这很复杂,这就是 infer* 函数发挥作用的地方。他们实际上除了让 TypeScript 推断类型外什么都不做,因为我们没有为内部函数提供通用参数。

推断state的类型,然后根据typeof state推断schema的类型,然后我们可以将结果类型设置为ValidationResult<typeof state, typeof schema>,这给了我们完整的类型安全。

如果将上述代码放入 TypeScript playground 中,您可以通过将鼠标悬停在变量名称上来查看推断类型,如果您尝试更改名称和类型,您会看到编译器警告。当您开始输入时,您还应该得到 auto-complete 建议。