I need help with type declaration that contains a backward referring and I'll explain:
Given an interface Car like that:
interface Car {
name: string,
engine: Engine,
brand: Brand,
...
}
I need to create a type of the following structure:
[ 'engine', (engine1, engine2) => {...} ]
Where engine1 and engine2 will be the same type as Car['engine'] and without limitation of generality - I need an array of:
[(keyof T) as S, (s1: S, s2: S) => { ... }]
In words: an array that contains exactly two elements: the first - is a property of T and the second is a function that accepts arg1 and arg2 where their type is the same type as the type of T[the first item in the array]
Can someone help me to declare such a type?
This will do:
type MyType<T extends object> = { [S in keyof T]: [S, (s1: T[S], s2: T[S]) => any] }[keyof T];
const e: MyType<Car> = ['engine', (e1: Engine, e2: Engine) => { }];
Explanation (due to request in comment):
Just break it apart.
Consider type:
type MyType1<T extends object, S extends keyof T> = [S, (s1: T[S], s2: T[S]) => any];
This type seems pretty obvious. But has one disadvantage - you must type key name explicitly.
const e: MyType1<Car, 'engine'> = ['engine', (e1: Engine, e2: Engine) => { }];
So you make union of all MyType1 possibilities for each property key:
type MyType2<T extends object> = { [S in keyof T]: MyType1<T, S> }[keyof T];
The original type above was just these two steps in one ;).
There is maybe a better approach but this is what I have.
I can't see a way to do it with a "plain" type so my suggestion is to create a "factory" - basically a function to create this kind of array.
This gives more flexibility with creating types.
type Engine = {
model: number;
}
type Brand = {
name: string;
}
interface Car {
name: string;
engine: Engine;
brand: Brand;
}
interface ArrayFactory<T> {
<U extends keyof T>(prop: U): [U, (arg1: T[U], arg2: T[U]) => void];
}
const createArray: ArrayFactory<Car> = (prop) => [prop, (arg1, arg2) => console.log(arg1, arg2)];
const arr = createArray('engine');
console.log(arr[0] === 'engine1');
arr[1]({model: 1}, {model: 2})
playground
Related
I would like to have a generic type predicate that allows me to check the type of an object's property.
To illustrate this, I want to achieve the following:
const a: { [key in string]: string | string[]} = {
arrayProp: ["this", "is", "an", "array"],
otherProp: "this is a string"
};
Object.keys(a).forEach((prop: keyof typeof a) => {
if (isArrayProperty(a, prop)) {
// `a[prop]` is correctly detected as array
a[prop].push("another value")
}
})
I was expecting something like this to work
function isArrayProperty<T, P extends keyof T>(
obj: T,
prop: P,
): T[P] is Array<any> {
return Array.isArray(obj[prop]);
}
However TypeScript seems to have problems with the generics and is statement in the return type.
Additional notes
I know I could just pass the value to a function like Array.isArray(a["arrayProp"]) to get it work.
However, I want to go even one step further where I pass in a constructor and a property to see whether the property of an object is an array type:
type ClassConstr<T> = new (...props) => T
function isArrayProperty<T, P extends keyof T>(
obj: ClassConstr<T>,
prop: P,
): T[P] is Array<any> {
return // some magic happening here;
}
class A {
someProp: any;
}
const a = new A()
a = ["some", "array"];
if (isArrayProperty(A, "someProp") {
// `a.someProp` is correctly detected as array
a.someProp.push("new value");
}
The background is, that I have a separate schema definition for my classes that is only available at runtime.
These schema definitions then decide, whether a property is an array, a string, a date, ...
Therefore, I would like to have a function that allows me to still achieve type-safety in the components where I use these classes.
Consider this:
const a: { [key in string]: string | string[] } = {
arrayProp: ["this", "is", "an", "array"],
otherProp: "this is a string"
};
function isArrayProperty<T, P extends keyof T>(
obj: T,
prop: P,
): obj is T & Record<P, Array<any>> {
return Array.isArray(obj[prop]);
}
Object.keys(a).forEach((prop: keyof typeof a) => {
if (isArrayProperty(a, prop)) {
a[prop].push("another value")
}
})
Playground
You need to assure TS that obj in isArrayProperty is a record with prop P and value Array<any>
Here is my code
async getAll(): Promise<GetAllUserData[]> {
return await dbQuery(); // dbQuery returns User[]
}
class User {
id: number;
name: string;
}
class GetAllUserData{
id: number;
}
getAll function returns User[], and each element of array has the name property, even if its return type is GetAllUserData[].
I want to know if it is possible "out of the box" in TypeScript to restrict an object only to properties specified by its type.
I figured out a way, using built-in types available since TypeScript version 3, to ensure that an object passed to a function does not contain any properties beyond those in a specified (object) type.
// First, define a type that, when passed a union of keys, creates an object which
// cannot have those properties. I couldn't find a way to use this type directly,
// but it can be used with the below type.
type Impossible<K extends keyof any> = {
[P in K]: never;
};
// The secret sauce! Provide it the type that contains only the properties you want,
// and then a type that extends that type, based on what the caller provided
// using generics.
type NoExtraProperties<T, U extends T = T> = U & Impossible<Exclude<keyof U, keyof T>>;
// Now let's try it out!
// A simple type to work with
interface Animal {
name: string;
noise: string;
}
// This works, but I agree the type is pretty gross. But it might make it easier
// to see how this works.
//
// Whatever is passed to the function has to at least satisfy the Animal contract
// (the <T extends Animal> part), but then we intersect whatever type that is
// with an Impossible type which has only the keys on it that don't exist on Animal.
// The result is that the keys that don't exist on Animal have a type of `never`,
// so if they exist, they get flagged as an error!
function thisWorks<T extends Animal>(animal: T & Impossible<Exclude<keyof T, keyof Animal>>): void {
console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`);
}
// This is the best I could reduce it to, using the NoExtraProperties<> type above.
// Functions which use this technique will need to all follow this formula.
function thisIsAsGoodAsICanGetIt<T extends Animal>(animal: NoExtraProperties<Animal, T>): void {
console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`);
}
// It works for variables defined as the type
const okay: NoExtraProperties<Animal> = {
name: 'Dog',
noise: 'bark',
};
const wrong1: NoExtraProperties<Animal> = {
name: 'Cat',
noise: 'meow'
betterThanDogs: false, // look, an error!
};
// What happens if we try to bypass the "Excess Properties Check" done on object literals
// by assigning it to a variable with no explicit type?
const wrong2 = {
name: 'Rat',
noise: 'squeak',
idealScenarios: ['labs', 'storehouses'],
invalid: true,
};
thisWorks(okay);
thisWorks(wrong1); // doesn't flag it as an error here, but does flag it above
thisWorks(wrong2); // yay, an error!
thisIsAsGoodAsICanGetIt(okay);
thisIsAsGoodAsICanGetIt(wrong1); // no error, but error above, so okay
thisIsAsGoodAsICanGetIt(wrong2); // yay, an error!
Typescript can't restrict extra properties
Unfortunately this isn't currently possible in Typescript, and somewhat contradicts the shape nature of TS type checking.
Answers in this thread that relay on the generic NoExtraProperties are very elegant, but unfortunately they are unreliable, and can result in difficult to detect bugs.
I'll demonstrate with GregL's answer.
// From GregL's answer
type Impossible<K extends keyof any> = {
[P in K]: never;
};
type NoExtraProperties<T, U extends T = T> = U & Impossible<Exclude<keyof U, keyof T>>;
interface Animal {
name: string;
noise: string;
}
function thisWorks<T extends Animal>(animal: T & Impossible<Exclude<keyof T, keyof Animal>>): void {
console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`);
}
function thisIsAsGoodAsICanGetIt<T extends Animal>(animal: NoExtraProperties<Animal, T>): void {
console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`);
}
const wrong2 = {
name: 'Rat',
noise: 'squeak',
idealScenarios: ['labs', 'storehouses'],
invalid: true,
};
thisWorks(wrong2); // yay, an error!
thisIsAsGoodAsICanGetIt(wrong2); // yay, an error!
This works if at the time of passing an object to thisWorks/thisIsAsGoodAsICanGet TS recognizes that the object has extra properties. But in TS if it's not an object literal, a value can always have extra properties:
const fun = (animal:Animal) =>{
thisWorks(animal) // No Error
thisIsAsGoodAsICanGetIt(animal) // No Error
}
fun(wrong2) // No Error
So, inside thisWorks/thisIsAsGoodAsICanGetIt you can't trust that the animal param doesn't have extra properties.
Solution
Simply use pick (Lodash, Ramda, Underscore).
interface Narrow {
a: "alpha"
}
interface Wide extends Narrow{
b: "beta"
}
const fun = (obj: Narrow) => {
const narrowKeys = ["a"]
const narrow = pick(obj, narrowKeys)
// Even if obj has extra properties, we know for sure that narrow doesn't
...
}
Typescript uses structural typing instead of nominal typing to determine type equality. This means that a type definition is really just the "shape" of a object of that type. It also means that any types which shares a subset of another type's "shape" is implicitly a subclass of that type.
In your example, because a User has all of the properties of GetAllUserData, User is implicitly a subtype of GetAllUserData.
To solve this problem, you can add a dummy property specifically to make your two classes different from one another. This type of property is called a discriminator. (Search for discriminated union here).
Your code might look like this. The name of the discriminator property is not important. Doing this will produce a type check error like you want.
async function getAll(): Promise<GetAllUserData[]> {
return await dbQuery(); // dbQuery returns User[]
}
class User {
discriminator: 'User';
id: number;
name: string;
}
class GetAllUserData {
discriminator: 'GetAllUserData';
id: number;
}
I don't think it's possible with the code structure you have. Typescript does have excess property checks, which sounds like what you're after, but they only work for object literals. From those docs:
Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments.
But returned variables will not undergo that check. So while
function returnUserData(): GetAllUserData {
return {id: 1, name: "John Doe"};
}
Will produce an error "Object literal may only specify known properties", the code:
function returnUserData(): GetAllUserData {
const user = {id: 1, name: "John Doe"};
return user;
}
Will not produce any errors, since it returns a variable and not the object literal itself.
So for your case, since getAll isn't returning a literal, typescript won't do the excess property check.
Final Note: There is an issue for "Exact Types" which if ever implemented would allow for the kind of check you want here.
Following up on GregL's answer, I'd like to add support for arrays and make sure that if you've got one, all the objects in the array have no extra props:
type Impossible<K extends keyof any> = {
[P in K]: never;
};
export type NoExtraProperties<T, U extends T = T> = U extends Array<infer V>
? NoExtraProperties<V>[]
: U & Impossible<Exclude<keyof U, keyof T>>;
Note: The type recursion is only possible if you've got TS 3.7 (included) or above.
The accepted answer, with a discriminator, is right. TypeScript uses structural typing instead of nominal typing. It means that the transpiler will check to see if the structure match. Since both classes (could be interface or type) has id of type number it matches, hence interchangeable (this is true one side since User is having more properties.
While this might be good enough, the issue is that at runtime the returned data from your method getAll will contains the name property. Returning more might not be an issue, but could be if you are sending back the information somewhere else.
If you want to restrict the data to only what is defined in the class (interface or type), you have to build or spread a new object manually. Here is how it can look for your example:
function dbQuery(): User[] {
return [];
}
function getAll(): GetAllUserData[] {
const users: User[] = dbQuery();
const usersIDs: GetAllUserData[] = users.map(({id}) => ({id}));
return usersIDs;
}
class User {
id: number;
name: string;
}
class GetAllUserData {
id: number;
}
Without going with the runtime approach of pruning the fields, you could indicate to TypeScript that both classes are different with a private field. The code below won't let you return a User when the return type is set to GetAllUserData
class User {
id: number;
name: string;
}
class GetAllUserData {
private _unique: void;
id: number;
}
function getAll(): GetAllUserData[] {
return dbQuery(); // Doesn't compile here!
}
I found this another workaround:
function exactMatch<A extends C, B extends A, C = B>() { }
const a = { a: "", b: "", c: "" }
const b = { a: "", b: "", c: "", e: "" }
exactMatch<typeof a, typeof b>() //invalid
const c = { e: "", }
exactMatch<typeof a, typeof c>() //invalid
const d = { a: "", b: "", c: "" }
exactMatch<typeof a, typeof d>() //valid
const e = {...a,...c}
exactMatch<typeof b, typeof e>() //valid
const f = {...a,...d}
exactMatch<typeof b, typeof f>() //invalid
See the original Post
Link to Playground
As an option, you can go with a hack:
const dbQuery = () => [ { name: '', id: 1}];
async function getAll(): Promise<GetAllUserData[]> {
return await dbQuery(); // dbQuery returns User[]
}
type Exact<T> = {[k: string | number | symbol]: never} & T
type User = {
id: number;
name: string;
}
type GetAllUserData = Exact<{
id: number;
}>
Error this produces:
Type '{ name: string; id: number; }[]' is not assignable to type '({ [k: string]: never; [k: number]: never; [k: symbol]: never; } & { id: number; })[]'.
Type '{ name: string; id: number; }' is not assignable to type '{ [k: string]: never; [k: number]: never; [k: symbol]: never; } & { id: number; }'.
Type '{ name: string; id: number; }' is not assignable to type '{ [k: string]: never; [k: number]: never; [k: symbol]: never; }'.
Property 'name' is incompatible with index signature.
Type 'string' is not assignable to type 'never'.
When using types instead of interfaces, the property are restricted. At least in the IDE (no runtime check).
Example
type Point = {
x: number;
y: number;
}
const somePoint: Point = {
x: 10,
y: 22,
z: 32
}
It throws :
Type '{ x: number; y: number; z: number; }' is not assignable to type 'Point'. Object literal may only specify known properties, and 'z' does not exist in type 'Point'.
I think types are good for defining closed data structures, compared to interfaces. Having the IDE yelling (actually the compiler) when the data does not match exactly the shape is already a great type guardian when developping
I managed to type the object currentProps so TS knows which properties it haves AND each property has its individual type (not an union of all possible types).
So far so good.
Then I have this generic function overrideForIndex which gets one of the possible properties, and should return its value. But it can't be assigned, because its return-type is a union, and not the specific type.
Now I could just cast it as any and call it a day, but I am curious if there is a way to handle this properly without casting to any.
Here is the "simplified" example:
(open in TS playground)
export type MyProps = {
id?: number;
title?: string;
}
const props: Record<keyof MyProps, any> = {
id: 123,
title: 'foo'
}
export const propNames = Object.keys(props) as Array<keyof MyProps>;
const data: Record<number, MyProps> = { 0: { id: 123, title: 'foo' }};
const buildLatestProps = (): { [P in keyof Required<MyProps>]: MyProps[P] } => {
const getLatest = <T extends keyof MyProps>(propName: T) => data[0][propName];
return Object.fromEntries(propNames.map(n => [n, getLatest(n)])) as any;
};
const currentProps = buildLatestProps();
const overrideForIndex = <T extends keyof MyProps>(propName: T, index: number): MyProps[T] =>
data[index][propName];
propNames.forEach(n => (currentProps[n] = overrideForIndex(n, 0) /* as any */)); // ERR: Type 'string | number | undefined' is not assignable to type 'undefined'.
If you take a look at these lines, the types are acting how you are expecting them to.
currentProps["id"] = overrideForIndex("id", 0);
currentProps["title"] = overrideForIndex("title", 0);
However, when you use the forEach loop, you are effectively using the following, where n is the union type of the key names of MyProps:
propNames.forEach((n: keyof MyProps) => { currentProps[n] = overrideForIndex(n, 0) });
Currently, TypeScript cannot untangle this. In addition, there is a great thread on why such a change could lead to unintentional consequences.
You should avoid using any in 99% of "use cases" for it as you are effectively turning TypeScript off. In most instances where you would use any, Record<KeyType, ValueType> is sufficient like so:
propNames.forEach(n => ((currentProps as Record<typeof n, unknown>)[n] = overrideForIndex(n, 0)));
In that block, you effectively suppress the error without relying on turning TypeScript completely off. It will also throw an error if you did type propNames as Array<keyof MyTypeExtendingMyProps>.
This question is very similar to a previous one I made.
I recommend reading my earlier question first: How to copy the structure of one generic type to another generic in TypeScript?
Rather than cloning the structure of a flat object type, I'm looking to clone the structure of a nested object type.
In orther words, I'm looking for a function that, given...
// ...this input type
interface NestedInput {
name: string;
arr: [
string,
Date,
{a: boolean}
];
nestedObject: {
x: number;
y: number;
};
}
// ...it produces this output type
type StringMethod = (val: string) => void;
type DateMethod = (val: Date) => void;
type NumMethod = (val: number) => void;
type BoolMethod = (val: boolean) => void;
interface NestedOutput {
name: StringMethod;
arr: [
StringMethod,
DateMethod,
{
a: BoolMethod;
}
];
nestedObject: {
x: NumberMethod;
y: NumberMethod;
}
}
Once again, it must be completely type-safe, such that I can access output.nestedObject.x or output.arr[2].a using intellisense.
I've been racking my brain the last 2 days trying to figure this out, so any help would be greatly appreciated!
PS: You may have noticed that we run into the problem of defining when we traverse a nested object. A Date object for instance wouldn't be traversed, but some other structure might. To prevent this from being a problem, you can assume that if the object is a vanilla JS object (see function below), then it's ok to traverse.
const getClass: (object: any) => string = Function.prototype.call.bind(Object.prototype.toString);
const isVanillaObject = (obj: any) => {
return getClass(obj) === "[object Object]";
}
You can use extends to switch on types, a mapped type for the object case and use recursion to allow deep nesting:
interface NestedInput {
name: string;
arr: [string, Date, { a: boolean }];
nestedObject: {
x: number;
y: number;
};
}
type StringMethod = (val: string) => void;
type DateMethod = (val: Date) => void;
type NumMethod = (val: number) => void;
type BoolMethod = (val: boolean) => void;
type Methodify<T> = T extends string
? StringMethod
: T extends Date
? DateMethod
: T extends number
? NumMethod
: T extends boolean
? BoolMethod
: {
[K in keyof T]: Methodify<T[K]>;
};
type Output = Methodify<NestedInput>;
//Results in:
type Output = {
name: StringMethod;
arr: [StringMethod, DateMethod, {
a: BoolMethod;
}];
nestedObject: {
x: NumMethod;
y: NumMethod;
};
}
I want to type a function called pick, I know typescript has built-in Pick<T, K>, now I want to implement the actual source code usage part, but I'm stuck.
What this function do is to pick provided object's property based on given strings and return new object.
The usage is this:
const data = { name: 'Jason', age: 18, isMember: true };
const pickedData = pick(data, ['name', 'isMember']);
console.log(pickedData)
// => { name: 'Jason', isMember: true }
My pick function's javascript implementation without typescript is this:
function pick(obj, properties) {
let result = {}
properties.forEach(property => {
result[property] = obj[property];
});
return result;
}
Here is the typescript version, but it's not compiling:
function pick<T, K extends keyof T>(obj: T, properties: K[]): Pick<T, K> {
let result: Pick<T, K> = {}; // Typescript yells: Type '{}' is not assignable to type 'Pick<T, K>'.
properties.forEach(property => {
result[property] = obj[property];
});
return result;
}
My VSCode screenshot:
But, there is one solution, I don't know if this is a good solution, that is to add as Pick<T, K>:
function pick<T, K extends keyof T>(obj: T, properties: K[]): Pick<T, K> {
let result: Pick<T, K> = {} as Pick<T, K> ; // Typescript now satisfy with this.
properties.forEach(property => {
result[property] = obj[property];
});
return result;
}
However, when using this pick function, typescript doesn't show the picked object property, instead, it shows unnecessary information:
It shows me this:
// typescript shows me, not what I want.
const pickedData: Pick<{
name: string;
age: number;
isMember: boolean;
}, "name" | "isMember">
Is there any way that typescript can show me this?
// This is what I want typescript to show me.
const pickedData: {
name: string;
isMember: boolean;
}
The first problem is that {} should not be assignable to Pick<T, K> since Pick<T, K> will probably have required properties. There are some instances where a type assertion is the only way to get things to work and this is one of those. Typescript does not know that you plan to assign those properties a bit later, you have that information and provide it to the compiler in the form of the type assertion, basically saying, relax, I know this isn't really Pick<T, K> but I will make it into that shortly.
The second part of you issue is not so much a functional one Pick<{ name: string; age: number; isMember: boolean;, "name" | "isMember"> is structurally equivalent to { name: string; isMember: boolean; }. Sometimes the language service expands these types, sometimes it does not. This is a recent issue describing this exact behavior.
You can force an expansion of the type with a more complicated type (an identity mapped type in an intersection with {}):
type Id<T extends object> = {} & { [P in keyof T]: T[P] }
function pick<T, K extends keyof T>(obj: T, properties: K[]): Id<Pick<T, K>> {
let result: Pick<T, K> = {} as Pick<T, K> ; // Typescript now satisfy with this.
properties.forEach(property => {
result[property] = obj[property];
});
return result;
}
const data = { name: 'Jason', age: 18, isMember: true };
const pickedData = pick(data, ['name', 'isMember']);
console.log(pickedData)
Just as a warning, there are no guarantees Id will always be expanded, all I can say is that in the current version Id seems to always force the expansion of the mapped type.