Welcome toVigges Developer Community-Open, Learning,Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
183 views
in Technique[技术] by (71.8m points)

How to provide type-saferty for the objects with many properties in TypeScript without type annotation omitting?

My issue AsValue utility type feature request in TypeScript repository has been declined and Nov 2020. I expected the below API:

const MarkupPreprocessingDefaultSettings: AsValue = {
  mustExecuteCodeQualityInspection: true,
  waitingForOtherFilesWillBeSavedPeriod__milliseconds: 1000,
  entryPointsGroupDependent: {
    indentationSpacesCountInOutputCode: 2,
    mustExecuteHTML5_Validation: true,
    mustExecuteAccessibilityInspection: true
  }
};

// Same as
const MarkupPreprocessingDefaultSettings: {
  mustExecuteCodeQualityInspection: true,
  waitingForOtherFilesWillBeSavedPeriod__milliseconds: 1000,
  entryPointsGroupDependent: {
    indentationSpacesCountInOutputCode: 2,
    mustExecuteHTML5_Validation: true,
    mustExecuteAccessibilityInspection: true
  }
} = {
  mustExecuteCodeQualityInspection: true,
  waitingForOtherFilesWillBeSavedPeriod__milliseconds: 1000,
  entryPointsGroupDependent: {
    indentationSpacesCountInOutputCode: 2,
    mustExecuteHTML5_Validation: true,
    mustExecuteAccessibilityInspection: true
  }
};

O'K, but what we have instead in reality? Let's consider the concrete problem.

Targets

Annotate the routing variable such as:

  1. TypeScript must to know which properties are exists, and which are no. For example, routing.products must work, but routing.gibberish must cast the TypeScript error.
  2. The count of keys in addition to products and orders could be arbitrary large.
  3. None of type annotating omitting, any and object allowed.
type Route = {
    URN: string;
}

const routing /* : ??? */ = {
    products: {
        URN: "/products"
    },
    orders: {
        URN: "/orders"
    }
}

Option 1: Annotate everything manually (overkill)

const routing : {
    products: Route;
    orders: Route;
    // and several thousands of keys for real commercial application ...
} = {
    products: {
        URN: "/products"
    },
    orders: {
        URN: "/orders"
    },
    // and several thousands of keys for real commercial application ...
}

Conclusion: non-technological, hard to extend and maintenance. Unacceptable.

Option 2: Use indexed type (loose type saferty)

type Route = {
    URN: string;
}

const Routing: { [routeName: string]: Route  } = {
    products: {
        URN: "/products"
    },
    orders: {
        URN: "/orders"
    }
}

console.log(Routing.products)
console.log(Routing.orders)
console.log(Routing.gibberish) // Undefined!

Conclusion: it will not prevent the typo; no autocomplete available. Unacceptable.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

Option 3: Use a generic helper function

const asRouting = <T extends Record<keyof T, Route>>(t: T) => t;

const routing = asRouting({
   products: {
      URN: "/products"
   },
   orders: {
      URN: "/orders"
   }
});

If you annotate the routing variable at all, the compiler will treat the variable as being of the annotated type only, and you won't get any type inference. As you see, either you have to annotate with exactly the right type, which is redundant, or you annotate a type which is wider than you want, and you lose type information you care about. So you really shouldn't annotate routing. Let the compiler infer its type for you.

But what if you mistakenly initialize routing with a value where one or more of the properties is not a valid Route? Well, the code that uses routing will yield a compiler error. But that is likely to be quite far from the definition of routing. You'd like the compiler error to be right there so you can fix it.

That's where the generic helper function above comes in. asRouting() is an identity function at runtime, and just outputs the same value as the input. At compile time, it is also an identity function, and the output type will be the same as the input type. But since this type, T, is constrained to Record<keyof T, Route>, asRouting() will only accept inputs where all its properties are valid Route objects.

Let's see what happens with routing as defined above. Its inferred type can be shown via IntelliSense to be:

/* const routing: {
    products: {
        URN: string;
    };
    orders: {
        URN: string;
    };
} */

Which is exactly the type you want, without requiring that you spell it out:

console.log(routing.products.URN); // okay
console.log(routing.orders.URN); // okay
console.log(routing.somethingElse); // compiler error!
// ---------------> ~~~~~~~~~~~~~
// Property 'somethingElse' does not exist

But you still get all the type checking you need to ensure that all properties are Route:

const badRouting = asRouting({
   products: {
      URL: "/product"; // error! 
   // ~~~~~~~~~~~~~~~~ 
   //  Object literal may only specify known properties, 
   //  and 'URL' does not exist in type 'Route'
   }
})

Playground link to code


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to Vigges Developer Community for programmer and developer-Open, Learning and Share
...