TypeScript: Recovering the "object literal may only specify known properties" check when passing through a generic function - type-inference

I realize the "object literal may only specify known properties" doesn't work in all cases. In particular, it looks like it doesn't work when I pass the object literal through an identity-like function (playground link)
declare function setUser(arg: {name: string}): void;
declare function identity<T>(v: T): T;
setUser({name: 'a', age: 12}); // Error, good!
setUser(identity({name: 'a', age: 12})); // No error, sad.
const u = {name: 'a', age: 12};
setUser(u); // No error, but I'm used to this case already.
Is there a way to write identity in a way that will get back the error?
In my actual codebase, I'm not using the identity function, but a slight variant (playground link):
declare function setUser(arg: {name: string}): void;
type NonNullable<T> = T extends null ? never : T;
export type NullableToOptional<T> = {
[K in keyof T]: null extends T[K] ? NonNullable<T[K]> | undefined : T[K];
};
export function toOptional<T>(x: T): NullableToOptional<T> {
return x as NullableToOptional<T>;
}
setUser({name: 'a', age: 12}); // Error, good!
setUser(toOptional({name: 'a', age: 12})); // No error, sad.

Here you have a naive implementation. You will find explanation in comments
type Base = { name: string }
// credits goes to https://github.com/microsoft/TypeScript/issues/40248
type IsNever<T> = [T] extends [never] ? true : false;
/**
* Obtain extra keys
*/
type ExtraKeys<T, U> = Exclude<keyof U, keyof T>
type Validator<Obj> = (
Obj extends Base
? (
Base extends Obj
? Obj : (
/**
* If there are no extra keys
*/
IsNever<ExtraKeys<Base, Obj>> extends true
/**
* Return source object
*/
? Obj
/**
* Otherwise return source object where all extra keys are [never]
* It helps TS compiler to highlight only invalid props
*/
: Base & Record<ExtraKeys<Base, Obj>, never>
)
)
: never
)
/**
* Validator - validates the argument
*/
declare function setUser<
Name extends string,
Obj extends { name: Name }
>(arg: Validator<Obj>): void;
type NonNullable<T> = T extends null ? never : T;
export type NullableToOptional<T> = {
[K in keyof T]: null extends T[K] ? NonNullable<T[K]> | undefined : T[K];
};
/**
* We need to infer each key and value pair in order to be alphabetic
* to validate return type
*/
export function toOptional<
Prop extends PropertyKey,
Value extends string | number,
Obj extends Record<Prop, Value>>(x: Obj) {
return x as NullableToOptional<Obj>;
}
setUser({ name: 'a', age: 2 }); // Error
setUser(toOptional({ name: 'a', age: 12 })); // Error
setUser({ name: 'a' }); // Error
setUser(toOptional({ name: 'a' })); // Error
Playground

Related

Issues generating an OpenAPI spec using Micronaut-openapi for sealed Kotlin classes

I'm having trouble with sealed classes. I get a specification from Micronaut-openapi, but the code generator I am using (orval) experiences a cyclic reference and fails.
Given this data class:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type",
visible = true
)
#JsonSubTypes(
JsonSubTypes.Type(name = "lounge", value = AnonymousResponse.Lounge::class),
JsonSubTypes.Type(name = "diningRoom", value = AnonymousResponse.DiningRoom::class)
)
sealed class AnonymousResponse {
abstract val id: Int
#JsonTypeName("lounge")
data class Lounge(
override val id: Int,
val hasTv: Boolean,
) : AnonymousResponse()
#JsonTypeName("diningRoom")
data class DiningRoom(
override val id: Int,
val hasTable: Boolean,
) : AnonymousResponse()
}
Micronaut-openapi generates the following components:
components:
schemas:
AnonymousResponse:
type: object
properties:
id:
type: integer
format: int32
discriminator:
propertyName: type
mapping:
lounge: '#/components/schemas/AnonymousResponse.Lounge'
diningRoom: '#/components/schemas/AnonymousResponse.DiningRoom'
oneOf:
- $ref: '#/components/schemas/AnonymousResponse.Lounge'
- $ref: '#/components/schemas/AnonymousResponse.DiningRoom'
AnonymousResponse.DiningRoom:
allOf:
- $ref: '#/components/schemas/AnonymousResponse'
- required:
- hasTable
- id
type: object
properties:
id:
type: integer
format: int32
hasTable:
type: boolean
AnonymousResponse.Lounge:
allOf:
- $ref: '#/components/schemas/AnonymousResponse'
- required:
- hasTv
- id
type: object
properties:
id:
type: integer
format: int32
hasTv:
type: boolean
Which leads to the following error in orval:
src/models/anonymousResponseDiningRoom.ts:10:13 - error TS2456: Type alias 'AnonymousResponseDiningRoom' circularly references itself.
10 export type AnonymousResponseDiningRoom = AnonymousResponse & AnonymousResponseDiningRoomAllOf;
~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/models/anonymousResponse.ts:11:13 - error TS2456: Type alias 'AnonymousResponse' circularly references itself.
11 export type AnonymousResponse = AnonymousResponseLounge | AnonymousResponseDiningRoom | AnonymousResponseOneOf;
~~~~~~~~~~~~~~~~~
src/models/anonymousResponseLounge.ts:10:13 - error TS2456: Type alias 'AnonymousResponseLounge' circularly references itself.
10 export type AnonymousResponseLounge = AnonymousResponse & AnonymousResponseLoungeAllOf;
~~~~~~~~~~~~~~~~~~~~~~~
I am not entirely sure whether it's the specification generator or the code generator doing something wrong, but the "allOf" - AnonymousResponse references looks iffy to me, as (at least from how I read it) it would lead to e.g. Lounge also containing information from DiningRoom?
After trying placing a #Schema annotation on the sealed class, and looking into how to customise the automatic Schema generation, I realised this worked:
// No Schema here
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type",
visible = true
)
#JsonSubTypes(
JsonSubTypes.Type(name = "lounge", value = AnonymousResponse.Lounge::class),
JsonSubTypes.Type(name = "diningRoom", value = AnonymousResponse.DiningRoom::class)
)
sealed class AnonymousResponse {
abstract val id: Int
#Schema // Schema here
data class Lounge(
override val id: Int,
val hasTv: Boolean,
) : AnonymousResponse()
#Schema // Schema here
data class DiningRoom(
override val id: Int,
val hasTable: Boolean,
) : AnonymousResponse()
}
which ultimately resulted in the following orval code:
anonymousResponse.ts
import type { AnonymousResponseLounge } from './anonymousResponseLounge';
import type { AnonymousResponseDiningRoom } from './anonymousResponseDiningRoom';
import type { AnonymousResponseOneOf } from './anonymousResponseOneOf';
export type AnonymousResponse = AnonymousResponseLounge | AnonymousResponseDiningRoom | AnonymousResponseOneOf;
anonymousResponseLounge.ts
export interface AnonymousResponseLounge {
id: number;
hasTv: boolean;
type?: string;
}
anonymousResponseDiningRoom.ts
export interface AnonymousResponseLounge {
id: number;
hasTv: boolean;
type?: string;
}
anonymousResponseOneOf.ts
export type AnonymousResponseOneOf = {
id?: number;
};
And while it doesn't have constant values for the discriminator types, I don't think this is the fault of the spec.

Typescript how to declare a function that returns a lowest common denominator type?

i want declare a function that returns a common type or its extended type
interface Common {
id: number;
}
interface AdditionalInformation extends Common {
myname: string;
}
Surely the function returns an object containing the id property
and wishing it could also return the myname property
I tried to declare the function like this:
export class Lib {
public static lowestCommonDenominator <T extends Common>(): Common {
const a: Common = { id: 1 };
return a;
}
public static firstCaseFunction(): Common {
const ok: Common = this.lowestCommonDenominator();
return ok;
}
public static secondCaseFunction(): AdditionalInformation {
// Property 'myname' is missing in type 'Common' but required in type 'AdditionalInformation'.ts(2741)
const ko: AdditionalInformation = this.lowestCommonDenominator();
return ko;
}
}
But when I assign the function to an extended type, I get the error:
Property 'myname' is missing in type 'Common' but required in type
'AdditionalInformation'.ts(2741)
Is it possible to implement what I want?
This code snippet removes the error
export class Lib {
public static lowestCommonDenominator <T extends Common>(): T {
const a: Common = { id: 1 };
return a as T;
}
public static firstCaseFunction(): Common {
const ok: Common = this.lowestCommonDenominator();
return ok;
}
public static secondCaseFunction(): AdditionalInformation {
const ko: AdditionalInformation = this.lowestCommonDenominator<AdditionalInformation>();
return ko;
}
}

Using Typescript object spread operator with this keyword

In my code I often have to copy data from json to instantiate class in constructor.
function append(dst, src) {
for (let key in src) {
if (src.hasOwnProperty(key) {
dst[key] = src[key];
}
}
};
export class DataClass {
id: number;
title: string;
content: string;
img: null | string;
author: string;
// no methods, just raw data from API
}
export class AdoptedClass1 extends DataClass {
// has same fields as DataClass
showcase: string;
constructor (data: DataClass) {
append(data, this);
// do some stuff
}
}
// similar code for AdoptedClass2
I'm wondering if I can replace append function call in constructor with object spread operator
For your need I'll prefer to use Object.assign(this, data) over your custom made append function. Nevertheless have a look at the documentation to understand the limitation of it.
Back to your main question: it is not possible to use the spread operator to do what you want. Many people are interested in that feature but it has been put on hold as you can see here.
To get closer of what you ask we can refactor your code a little:
export class DataClass {
id: number
title: string
content: string
img: null | string
author: string
constructor(data: DataClass) {
Object.assign(this, data)
}
}
export class AdoptedClass1 extends DataClass {
showcase: string
constructor (data: DataClass) {
super(data)
// do some stuff
}
}
By simply adding the constructor to the data class you will be allowed to use super(data) in children and IMHO the code will be a lot cleaner.
You can use object spread operator by replacing this line:
append(data,this)
with this line
data = {...data, ...this};

How to take a subset of an object using an interface?

Suppose I have this class and interface
class User {
name: string;
age: number;
isAdmin: boolean;
}
interface IUser {
name: string;
age: number;
}
And then I get this json object from somewhere
const data = {
name: "John",
age: 25,
isAdmin: true
}
I want to subset data using IUser and remove the isAdmin property like this
let user = subset<IUser>(data);
// user is now { name: "John", age: 25 }
// can safely insert user in the db
My question is how do I implement that function in TypeScript?
function subset<T>(obj: object) {
// keep all properties of obj that are in T
// keep, all optional properties in T
// remove any properties out of T
}
There's no way to do that which is better than:
function subset(obj: IUser) {
return {
name: obj.name,
age: obj.age
}
}
The typescript interfaces don't exist at runtime (which is when subset is invoked) so you cannot use the IUser interface to know which properties are needed and which aren't.
You can use a class which does "survive" the compilation process but:
class IUser {
name: string;
age: number;
}
Compiles to:
var IUser = (function () {
function IUser() {
}
return IUser;
}());
As you can see, the properties aren't part of the compiled output, as the class members are only added to the instance and not to the class, so even a class won't help you.
You can use decorator and metadata (more on that here) but that sounds like an overkill for your scenario.
Another option for a more generic subset function is:
function subset<T>(obj: T, ...keys: (keyof T)[]) {
const result = {} as T;
keys.forEach(key => result[key] = obj[key]);
return result;
}
let user1 = subset(data, "name", "age");
let user2 = subset(data, "name", "ag"); // error: Argument of type '"ag"' is not assignable to parameter of type '"name" | "age" | "isAdmin"'

Real use of generic in typescript

I don't get what is the actual use of generics in typescirpt.
interface ICustomer
{
name: string;
age: number;
}
function CalcAverageAge<c extends ICustomer>(cust1: c, cust2: c): number
{
return (cust1.age + cust2.age)/2;
}
resNumber = CalcAverageCustomerAge({name: "Peter", age: 62},
{name: "Jason", age: 33});
In the above example we are passing interface c to function CalcAverageAge.
But without using extends ICustomer we can't use age and name inside that class.
Then what is the use of passing template( c ) in the function.
We can directly write the code in below format
function CalcAverageAge(cust1: ICustomer, cust2: ICustomer): number
{
return (cust1.age + cust2.age)/2;
}
Can you give a real example where generics is really useful?
I will explain you my scenario where I need to use generics.
interface t1{
a:String
b:number
}
interface t2 {
a:String
b:number
c:number
}
interface t3 {
a:String
b:number
d:number
}
class base<T extends t1> {
constructor( input : T, type:string ){
//some common code for both derived1 and derived2
if(type==="derived1"){
console.log(input.c);// will throw error because t1 doesn't contains c
} else if ( type==="derived2"){
console.log(input.d);// will throw error because t1 doesn't contains d
}
}
}
class derived1 extends<t2>{
constructor(){
var temp = {a:"11",b:2,c:3}
super(temp,"derived1");
}
class derived2 extends<t3>{
constructor(){
var temp = {a:"11",b:2,d:3}
super(temp,"derived2");
}
}
Can we achieve this with generice?
If not what would be the best way of implementation avoiding duplicate codes.
In your example it is correct that the interface is all you need.
Generics is something that is useful when you want to make something generic; sometimes it might be so generic that you do not even need an interface. The example you bring up is not only a generic, it also limits what the generic can look like with an interface.
Other examples of what a generic can be used for is a collection that can contain any type of item. The array type in typescript is an example of that - var a = new Array<number>() - for example.
But say that you want to create a function that compares two items, something like this:
interface IValue { value: number; }
function max(a: IValue, b: IValue): IValue {
return a.value > b.value ? a : b;
}
In this case you have the issue that the max function returns its result as an IValue In most cases this is not what you want. What you want is something like this:
interface IValue { value: number; }
function max<T extends IValue>(a: T, b: T): T {
return a.value > b.value ? a : b;
}
Here the return type of max is whatever the generic type T is, and this is more useful.