是否可以根据传入的对象推断对象类型?

时间:2021-05-13 17:12:05

标签: typescript typescript-generics

我有各种共享一组通用字段的文档。

我正在尝试创建一个函数,该函数将填充公共字段,同时将唯一字段作为输入,从而生成完整的文档。

我有这个工作,但现在我需要手动指定最终类型。我很好奇打字稿中是否有一种方法可以根据传入的字段推断最终类型(如果传入的字段与任何已知的文档类型不匹配,则抛出错误。

这是我现在的代码:

type DocA = {
  id: string,
  createdAt: string,
  onlyDocA: string
}

type DocB = {
  id: string,
  createdAt: string,
  onlyDocB: string
}

type AllDocTypes = DocA | DocB

export const createBaseDocument = <DocType extends AllDocTypes>(
  fields: Omit<DocType, 'id' | 'createdAt'>,
): DocType => {
  const createdAt = new Date();

  return {
    id: generateId(),
    createdAt: createdAt.toString(),
    ...fields,
  } as DocType
}

function generateId(): string {
  return 'id'
}

// testA should be of type `DocA`
const testA = createBaseDocument({ id: 'a', createdAt: '123', onlyDocA: 'hello'})

// testA should be of type `DocB`
const testB = createBaseDocument({ id: 'a', createdAt: '123', onlyDocB: 'hello'})

// testC should throw a type error
const testC = createBaseDocument({ id: 'a', createdAt: '123', onlyDocC: 'hello'})

// testD should throw a type error
const testD = createBaseDocument({ id: 'a', createdAt: '123', onlyDocA: 'hello', onlyDocB: 'hello'})

typescript playgroud

3 个答案:

答案 0 :(得分:2)

稍微颠倒您的逻辑即可使其工作。不幸的是,我不知道为什么你的方法会失败(但是Oleg Valter does)。

    compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.13'
    compile group: 'org.apache.httpcomponents', name: 'httpmime', version: '4.5.3'
    

try (CloseableHttpClient httpclient = HttpClients.createDefault()) {

        File file = new File("src/main/resources/48-1.jpg");

        MultipartEntityBuilder entitybuilder = MultipartEntityBuilder.create();
        entitybuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
        entitybuilder.addBinaryBody("image", new FileInputStream(file), ContentType.APPLICATION_OCTET_STREAM, file.getName());
        entitybuilder.setContentType(ContentType.create("multipart/related"));

        HttpEntity mutiPartHttpEntity = entitybuilder.build(); 

        RequestBuilder reqbuilder = RequestBuilder.post(url);
        reqbuilder.setEntity(mutiPartHttpEntity);

        HttpUriRequest multipartRequest = reqbuilder.build();

        HttpResponse httpresponse = httpclient.execute(multipartRequest);

        System.out.println("response status = " + httpresponse.getStatusLine().getStatusCode());
        System.out.println("filenet id = " + EntityUtils.toString(httpresponse.getEntity()));

    }catch(Exception e) {
        e.printStackTrace();
    }

Playground

它也不允许传入诸如 interface DocBase { id: string createdAt: string } interface OnlyDocA { onlyDocA: string } interface OnlyDocB { onlyDocB: string } type Onlys = OnlyDocA | OnlyDocB function createBaseDocument<T extends Onlys> (fields: T): DocBase & T { const createdAt = new Date(); return { id: generateId(), createdAt: createdAt.toString(), ...fields, } } function generateId(): string { return 'id' } const x = createBaseDocument({ onlyDocA: 'a' }) 之类的随机内容,除非您之前已经定义过。

答案 1 :(得分:2)

如果您想更好地控制进入函数的内容,您可能需要改用函数重载:

type DocA = {
  id: string,
  createdAt: string,
  onlyDocA: string
}

type DocB = {
  id: string,
  createdAt: string,
  onlyDocB: string
}

function createBaseDocument(fields: Omit<DocA, 'id'| 'createdAt'>): DocA;
function createBaseDocument(fields: Omit<DocB, 'id'| 'createdAt'>): DocB;
function createBaseDocument(fields: Omit<DocA, 'id'| 'createdAt'>|Omit<DocB, 'id'| 'createdAt'>): DocB|DocA{
  const createdAt = new Date();
  return {
    id:generateId(),
    createdAt: createdAt.toString(),
    ...fields
  }
}

这样 createBaseDocument({onlyDocA:'a'})createBaseDocument({onlyDocB:'b'}) 就可以了,但是 createBaseDocument({onlyDocA:'a',onlyDocB:'b'}) 会导致错误

答案 2 :(得分:2)

扩展Lazaranswer并解释原因:

您的方法失败了,因为 Omit<AllDocTypes, "id"|"createdAt"> 没有按照您的预期执行。您想使用它保留 onlyDocAonlyDocB 属性,但这不是它的工作原理。你实际得到的是一个空对象 {}:

type testOmit = Omit<AllDocTypes, "id"|"createdAt">; // {}

{} 类型在此上下文中实际上是一个非常宽的类型,表示“具有任意数量的任意类型属性的对象”。这就是为什么您可以在函数调用中指定任何内容。

至于为什么会发生这种情况,这是因为您将它与具有联合的泛型参数类型一起使用。编译器不知道什么具体类型被替换为参数,因此 Omit 助手与 DocA | DocB 的联合一起工作。

您可能知道,只有共享属性可以在对象联合上访问(否则将不安全),因此,在删除 "id""createdAt" 共享属性后,您将剩下一个空对象(onlyDocAonlyDocB 不共享)。


至于为什么将泛型与DocBase(<T extends Onlys> (fields: T): DocBase & T)相交并不能防止添加多余的属性,这是因为在没有显式参数注释的情况下,推断出T来自传入的参数,例如:

/**
function createBaseDocument<{
    onlyDocA: string;
    whatever: string;
}>(fields: {
    onlyDocA: string;
    whatever: string;
}): DocBase & {
    onlyDocA: string;
    whatever: string;
}*/
const z = createBaseDocument({ onlyDocA: "a", whatever: "whenever" }); // ok

要使解决方案起作用,您必须明确:

const x = createBaseDocument<OnlyDocA>({ onlyDocA: 'a' }); //ok
const y = createBaseDocument<OnlyDocA>({ onlyDocA: 'a', onlyDocB: 'b' ,onlyDocX: 'x' }); //error

Playground